pax_global_header00006660000000000000000000000064151074241150014512gustar00rootroot0000000000000052 comment=e9f24662e50b29c3e8eeba5a299780edcda9cc17 go-agent-3.42.0/000077500000000000000000000000001510742411500133015ustar00rootroot00000000000000go-agent-3.42.0/.github/000077500000000000000000000000001510742411500146415ustar00rootroot00000000000000go-agent-3.42.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001510742411500170245ustar00rootroot00000000000000go-agent-3.42.0/.github/ISSUE_TEMPLATE/bug-report---.md000066400000000000000000000016551510742411500216520ustar00rootroot00000000000000--- name: "Bug report \U0001F41B" about: Create a report to help us improve title: '' labels: bug assignees: '' --- [NOTE]: # ( ^^ Provide a general summary of the issue in the title above. ^^ ) ## Description [NOTE]: # ( Describe the problem you're encountering. ) [TIP]: # ( Do NOT give us access or passwords to your New Relic account or API keys! ) ## Steps to Reproduce [NOTE]: # ( Please be as specific as possible. ) ## Expected Behavior [NOTE]: # ( Tell us what you expected to happen. ) ## NR Diag results [NOTE]: # ( Provide any other relevant log data. ) ## Your Environment [TIP]: # ( Include as many relevant details about your environment as possible including the running version of New Relic software and any relevant configurations. ) ## Reproduction case [TIP]: # ( Link a sample application that demonstrates the issue. ) ## Additional context [TIP]: # ( Add any other context about the problem here. ) go-agent-3.42.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003201510742411500210070ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Troubleshooting url: https://github.com/newrelic/go-agent/blob/master/README.md#support about: checkout the README for troubleshooting directions go-agent-3.42.0/.github/ISSUE_TEMPLATE/enhancement-request---.md000066400000000000000000000013151510742411500235300ustar00rootroot00000000000000--- name: "Enhancement request \U0001F4A1" about: Suggest an idea for a future version of this project title: '' labels: enhancement assignees: '' --- [NOTE]: # ( ^^ Provide a general summary of the request in the title above. ^^ ) ## Summary [NOTE]: # ( Provide a brief overview of what the new feature is all about. ) ## Desired Behaviour [NOTE]: # ( Tell us how the new feature should work. Be specific. ) [TIP]: # ( Do NOT give us access or passwords to your New Relic account or API keys! ) ## Possible Solution [NOTE]: # ( Not required. Suggest how to implement the addition or change. ) ## Additional context [TIP]: # ( Why does this feature matter to you? What unique circumstances do you have? ) go-agent-3.42.0/.github/ISSUE_TEMPLATE/troubleshooting.md000066400000000000000000000007041510742411500225760ustar00rootroot00000000000000 We use GitHub to track feature requests and bug reports. Please **do not** submit issues for questions about how to configure, use features, troubleshoot, or best practices for using New Relic software. See the README.md troubleshooting section in this repository for more details on self-service troubleshooting tooling, links to our comprehenive documentation, and how to get further support. go-agent-3.42.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000011671510742411500204470ustar00rootroot00000000000000 ## Links ## Details go-agent-3.42.0/.github/auto_assign.yml000066400000000000000000000013341510742411500177010ustar00rootroot00000000000000# Set to true to add reviewers to pull requests addReviewers: true # Set to true to add assignees to pull requests addAssignees: true # A list of reviewers to be added to pull requests (GitHub user name) reviewers: - iamemilio - mirackara - nr-swilloughby # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) numberOfReviewers: 1 # A list of assignees, overrides reviewers if set # assignees: # - assigneeA # A number of assignees to add to the pull request # Set to 0 to add all of the assignees. # Uses numberOfReviewers if unset. # numberOfAssignees: 2 # A list of keywords to be skipped the process that add reviewers if pull requests include it # skipKeywords: # - wip go-agent-3.42.0/.github/dependabot.yml000066400000000000000000000007351510742411500174760ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" # Disable version updates for gomod dependencies, security updates don't use this configuration # See: https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates open-pull-requests-limit: 0 directory: "../v3" schedule: interval: "daily" commit-message: prefix: "security" prefix-development: "chore" include: "scope" go-agent-3.42.0/.github/workflows/000077500000000000000000000000001510742411500166765ustar00rootroot00000000000000go-agent-3.42.0/.github/workflows/autoassign.yml000066400000000000000000000002741510742411500216010ustar00rootroot00000000000000name: 'Auto Assign' on: pull_request: types: [opened, ready_for_review] jobs: add-reviews: runs-on: ubuntu-latest steps: - uses: kentaro-m/auto-assign-action@v2.0.0 go-agent-3.42.0/.github/workflows/repolinter.yml000066400000000000000000000022341510742411500216050ustar00rootroot00000000000000# NOTE: This file should always be named `repolinter.yml` to allow # workflow_dispatch to work properly name: Repolinter Action # NOTE: This workflow will ONLY check the default branch! # Currently there is no elegant way to specify the default # branch in the event filtering, so branches are instead # filtered in the "Test Default Branch" step. on: [push, workflow_dispatch] jobs: repolint: name: Run Repolinter runs-on: ubuntu-latest steps: - name: Test Default Branch id: default-branch uses: actions/github-script@v2 with: script: | const data = await github.repos.get(context.repo) return data.data && data.data.default_branch === context.ref.split('/').slice(-1)[0] - name: Checkout Self if: ${{ steps.default-branch.outputs.result == 'true' }} uses: actions/checkout@v2 - name: Run Repolinter if: ${{ steps.default-branch.outputs.result == 'true' }} uses: newrelic/repolinter-action@v1 with: config_url: https://raw.githubusercontent.com/newrelic/.github/main/repolinter-rulesets/community-plus.yml output_type: issue go-agent-3.42.0/.github/workflows/test-pull-request.yml000066400000000000000000000134441510742411500230460ustar00rootroot00000000000000# Copyright 2025 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # name: test-pull-request on: push: branches: - 'master' - 'develop' pull_request: jobs: setup-integration-matrix: runs-on: ubuntu-latest outputs: INTEGRATION_MATRIX: ${{ steps.setmatrix.outputs.INTEGRATION_MATRIX }} steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - id: setmatrix working-directory: ./go-agent run: | INTEGRATION_MATRIX=$(make integration-to-json) echo "INTEGRATION_MATRIX=$INTEGRATION_MATRIX" >> $GITHUB_OUTPUT setup-core-matrix: runs-on: ubuntu-latest outputs: CORE_MATRIX: ${{ steps.setmatrix.outputs.CORE_MATRIX }} steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - id: setmatrix working-directory: ./go-agent run: | CORE_MATRIX=$(make core-to-json) echo "CORE_MATRIX=$CORE_MATRIX" >> $GITHUB_OUTPUT gofmt-check: runs-on: ubuntu-latest strategy: matrix: go-version: [1.24.0, 1.25.0, stable] continue-on-error: true steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - name: Setup go uses: actions/setup-go@v5 with: go-version: ${{matrix.go-version}} cache: false - name: Display go version run: | go version - name: Run gofmt working-directory: ./go-agent/v3 run: | GOFMT_REPORTED_FILES="$(gofmt -l -e ./)" if [ ! -z "$GOFMT_REPORTED_FILES" ]; then gofmt -d -e ./ echo "### gofmt violations found in $(echo "$GOFMT_REPORTED_FILES" | wc -l) files" >> $GITHUB_STEP_SUMMARY echo "$GOFMT_REPORTED_FILES" >> $GITHUB_STEP_SUMMARY exit 1 fi govet-check: runs-on: ubuntu-latest strategy: matrix: go-version: [1.24.0, 1.25.0, stable] continue-on-error: true steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - name: Setup go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: false - name: Run go mod tidy working-directory: ./go-agent/v3 run: | go mod tidy shell: bash - name: Run go vet working-directory: ./go-agent/v3 run: | go vet ./... shell: bash core-tests: needs: setup-core-matrix runs-on: ${{ matrix.runner }} strategy: matrix: go-version: [1.24.0, 1.25.0, latest] core-test: ${{ fromJson(needs.setup-core-matrix.outputs.CORE_MATRIX) }} runner: [ubuntu-latest, ubuntu-24.04-arm] continue-on-error: true steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - name: Start test services working-directory: ./go-agent env: GO_VERSION: ${{ matrix.go-version }} PROFILE: ${{ matrix.core-test }} run: | make test-services-start - name: Run core tests working-directory: ./go-agent run: | docker exec -e TEST=${{ matrix.core-test }} -e COVERAGE=1 nr-go make core-test - name: Upload results to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: go-agent/v3/${{ matrix.core-test }}/coverage.txt - name: Stop services working-directory: ./go-agent run: | make test-services-stop integration-test: needs: setup-integration-matrix runs-on: ubuntu-latest strategy: matrix: go-version: [1.24.0, 1.25.0, latest] integration-test: ${{ fromJson(needs.setup-integration-matrix.outputs.INTEGRATION_MATRIX) }} continue-on-error: true steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - name: Start test services working-directory: ./go-agent env: GO_VERSION: ${{ matrix.go-version }} PROFILE: ${{ matrix.integration-test }} run: | make test-services-start - name: Run Integration tests working-directory: ./go-agent run: | docker exec -e TEST=${{ matrix.integration-test }} -e COVERAGE=1 nr-go make integration-test - name: Upload results to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: go-agent/v3/integrations/${{ matrix.integration-test }}/coverage.txt - name: Stop services working-directory: ./go-agent run: | make test-services-stop integration-test-arm64: needs: setup-integration-matrix runs-on: ubuntu-24.04-arm strategy: matrix: go-version: [1.24.0, 1.25.0, latest] integration-test: ${{ fromJson(needs.setup-integration-matrix.outputs.INTEGRATION_MATRIX) }} continue-on-error: true steps: - name: Checkout go-agent code uses: actions/checkout@v4 with: path: go-agent - name: Start test services working-directory: ./go-agent env: GO_VERSION: ${{ matrix.go-version }} PROFILE: ${{ matrix.integration-test }} run: | make test-services-start - name: Run Integration tests working-directory: ./go-agent run: | docker exec -e TEST=${{ matrix.integration-test }} nr-go make integration-test - name: Stop services working-directory: ./go-agent run: | make test-services-stop go-agent-3.42.0/.gitignore000066400000000000000000000000061510742411500152650ustar00rootroot00000000000000go.sumgo-agent-3.42.0/CHANGELOG.md000066400000000000000000004342461510742411500151270ustar00rootroot00000000000000## 3.42.0 ### Added * Added `ConfigTransactionEventsMaxSamplesStored`and `ConfigErrorCollectorMaxSamplesStored` allowing full control of maximum samples stored for Transaction Events, Custom Insights Events, Error Events, and Log Events * Added support for the `MultiValueHeaders` property when extracting the headers from `events.APIGatewayProxyResponse` in nrlambda * Thank you to community member @rittneje for contributing to this solution ### Fixed * Removed unused variables and modernize by replacing interface{} with any in the `nrpxg5` integration * Fixed a bug where error events did not correctly mark expected errors * Thank you to community member @driimus for contributing to this solution * Bumped `nrwriter` integration to use v1.0.2 * Thank you to community member @hiicharm for spotting this. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.41.0 ### Added * Added `DistributedTracer.Sampler` config options for controlling the sampling behavior for Inbound Requests for distributed traces * To configure the sampler, added `ConfigRemoteParentSampled(flag RemoteParentSamplingConfig)` and `ConfigRemoteParentNotSampled(flag RemoteParentSamplingConfig)` which handles sampling behavior based on what the remote parent has flagged * Flags added are `"always_on", "always_off", and "default"` which can be called using `RemoteParentSamplingConfig` * `Example: newrelic.ConfigRemoteParentSampled(newrelic.AlwaysOff)` * Added OOM Monitoring Tests * Increased Secure Agent Test Coverage ### Fixed * Updated third-party library versions due to reported security or other supportability issues: * `github.com/gofiber/fiber/v2` from 2.52.7 to 2.52.9 in `nrfiber` integration * `golang.org/x/net` from 0.25.0 to 0.38.0 in `nrconnect` integration ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.40.1 ### Fixed * Reverted utilization.go back to v3.39.0 release due to deadlock bug * Removed awssupport_test.go tests that added direct dependencies to go module ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.40.0 ### Added * Added `txn.IgnoreApdex()` function to ignore Apdex score for a given transaction * Added Custom Attributes via environment variables `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES` * Added `nrconnect` integration for connect library. Connect is a slim library for building browser and gRPC-compatible HTTP APIs * Thank you to community member @castaneai for contributing to this solution * Added `nrmongo-v2` integration supporting the mongodb-v2 library * Overhauled GitHub Actions Test Suite * Added dockerized database support for mongodb and pgx5 integrations ### Fixed * Enhanced query parameter representation in nrpgx5 integration * Fixed a bug where a race condition would occur in identifying container utilization. * Capture DynamoDB table name and index name in DatastoreSegment * Thank you to community member @rittneje for contributing to this solution * Updated third-party library versions due to reported security or other supportability issues: * `github.com/gofiber/fiber/v2` to 2.52.7 in `nrfiber` integration * `github.com/go-chi/chi/v5` to 5.2.2 in `nrgochi` integration ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.39.0 ### Added * Added nrfiber integration for go fiber framework * Thank you to community member @MitulShah1 for contributing to this solution * Updated nrslog example with new API * Thank you to community member @frankywahl for contributing to this solution * Add Optional Path Filtering Function to nrgin Middleware * Thank you to community member @frknikiz for contributing to this solution * Added ConfigDatastoreKeysEnabled to nrreddis integration allowing for reporting the names of keys along with the datastore operations * Switched GitHub Action Testing Suite from an Emulated ARM layer to Native ARM layers * Added support for time objects for attribute values for nrslog ### Fixed * Fixed linking metadata location in log messages ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.38.0 ### Added * Added new integration nrgochi v1.0.0 for support for go-chi library * Added IsEnded() method for the Transaction type. Allowing for a straightforward approach to checking if a transaction has ended * Community Member @frknikiz contributed to this solution ### Fixed * Added caveat to API docs about local log decoration in zap integration ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.37.0 ### Enhanced - Implemented a new approach to integrating New Relic with SLOG that is more lightweight, out of the way, and collects richer data. These changes have been constructed to be completely backwards-compatible with v1 of nrslog. Changes include: - Wrapping `slog.Handler` objects with errors to allow users to handle invalid use cases - A complete rework of log enrichment so that New Relic linking metadata does not invalidate JSON, BSON, or YAML scanners. This new approach will instead inject the linking metadata as a key-value pair. - Complete support for `With()`, `WithGroup()`, and attributes for automatic instrumentation. - Performance operations. - Robust testing (close to 90% coverage). - **This updates logcontext-v2/nrslog to v1.4.0.** - Now custom application tags (labels) may be added to all forwarded log events. - Enabled if `ConfigAppLogForwardingLabelsEnabled(true)` or `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED=TRUE` - May exclude labels named in `ConfigAppLogForwardingLabelsExclude("label1","label2",...)` or `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE="label1,label2,..."` - Labels are defined via `ConfigLabels(...)` or `NEW_RELIC_LABELS` - Added memory allocation limit detection/response mechanism to facilitate calling custom functions to perform application-specific resource management functionality, report custom metrics or events, or take other appropriate actions, in response to rising heap memory size. ### Fixed - Added protection around transaction methods to gracefully return when the transaction object is `nil`. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.36.0 ### Enhanced - Internal improvements to securityagent integration to better support trace handling and other support for security analysis of applications under test, now v1.3.4; affects the following other integrations: - nrecho, now v1.1.4 - nrecho-v4, now v1.1.3 - nrgin, now v1.3.3 - nrgorilla, now v1.2.3 - nrgraphqlgo, now v1.0.2 - nrhttprouter, now v1.1.3 ### Fixed - Added missing license files. - Fixed module dependencies in nrgrpc integration, now v1.4.5 - Corrects handling of `panic(nil)` to no longer try to keep pre-Go-1.21 behavior but to allow newer language semantics for that condition. Fixes [issue 975](https://github.com/newrelic/go-agent/issues/975). ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.35.1 ### Fixed - Security Agent Bug Hotfix: Do not update the security agent unti the go agent has completed its connect process [PR](https://github.com/newrelic/go-agent/pull/978) - Faster Trace ID generation - Community Member @ankon contributed [this change](https://github.com/newrelic/go-agent/pull/977) ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.35.0 ### Added - Enhanced security features (adds support for secure cookie even reporting) - Enables sharing of response headers with the csec-security-agent. - Now uses `error.Error()` value for log attributes - Thanks to @ejsolberg for the [PR](https://github.com/newrelic/go-agent/pull/947) - nramqp integration cloud services entity relationship changes - Enhances url support for amqp server connections ### Fixed - nrawssdk-v2 integration examples of `AppendMiddlewares` corrected. - Thanks to @Meroje for the [PR](https://github.com/newrelic/go-agent/pull/599) - Zerolog integration correction to example program `import` statement. - Fixes issue [#950](https://github.com/newrelic/go-agent/issues/950) - Zerolog integration JSON parser bug caused a runtime panic in some circumstances. - Fixes issue [#955](https://github.com/newrelic/go-agent/issues/955) - Fixed handling of `panic(nil)`. This was made necessary by changes introducted to Go as of 1.21. - A race condition was possible due to code level metrics accesses to a contended memory address. - Fixes issue [#949](https://github.com/newrelic/go-agent/issues/949) - Fixes issue [#957](https://github.com/newrelic/go-agent/issues/957) - Integer size issues flagged when converting unsigned to signed values. - Workflow corrections for CI processes in github. - Fixes issue [#946](https://github.com/newrelic/go-agent/issues/946) - Updated to use latest grpc and protobuf versions. - Fixes memory stat collection for `GOOS=js`. - Thanks @hslatman for the [PR](https://github.com/newrelic/go-agent/pull/967) ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.34.0 ### Added - logcontext-v2/nrlogrus can now collect user attributes - logcontext-v2/nrslog can now collect user attributes - use slog to manage Go agent logs with the new nrslog library - use zerolog go manage Go agent logs with the new nrzerolog library - support for `RegisterTLSConfig` in nrmysql ### Fixed - logcontext-v2/nrlogrus will still print user logs without isses if the Go agent has already been shut down. - switched protobuff to google.golang.org/protobuff in modfile ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.33.1 ### Added - Increased max span events default limit to 2,000 to align with agent specifications - Added support for gRPC API endpoints and HTTP status codes in the nrsecurity integration - Added feature to detect route of an incoming request for all supported frameworks in the nrsecurity integration. - Updated support for latest New Relic Security Agent release. ### Fixed - Fixed an issue with nrzap attributes not properly being forwarded - Improved comments on nropenai - Fixed a minor bug relating to ExpectStatusCodes in `app_run.go` ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.33.0 ### Added - Support for Zap Field Attributes - Updated dependency on csec-go-agent in nrsecurityagent ### Fixed - Fixed an issue where running containers on AWS would falsely flag Azure Utilization - Fixed a typo with nrecho-v3 - Changed nrslog example to use a context driven handler These changes increment the affected integration package version numbers to: - nrsecurityagent v1.3.1 - nrecho-v3 v1.1.1 - logcontext-v2/nrslog v1.2.0 - logcontext-v2/nrzap v1.2.0 ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. ## 3.32.0 ### Added * Updates to support for the New Relic security agent to report API endpoints. * Adds new wrapper function for the `nrecho`, `nrgin`, and `nrgorilla` integrations. * Handler to take New Relic transaction data from context automatically when using `nrslog` integration (thanks, @adomaskizogian!) ### Fixed * Adds missing license file to the `nropenai` integration. * Changes `*bedrockruntime.Client` parameters in `nrawsbedrock` integration to use a more general interface type, allowing the use of custom types which extend the bedrock client type. * Fixes `pgx5` pool example * Updated unit tests to check `Transaction.Ignore` * Updated `nrzap` unit tests to add background logger sugared test case. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.31.0 ### Added * Integration packages to instrument AI model invocations (see below). * New package nrawsbedrock v1.0.0 introduced to instrument calls to Amazon Bedrock Runtime Client API `InvokeModel` and `InvokeModelWithResponseStream` calls. Also provides a simple one-step method which invokes stream invocations and harvests the response stream data for you. * New package nropenai v1.0.0 introduced to instrument calls to OpenAI using `NRCreateChatCompletion`, `NRCreateChatCompletionStream`, and `NRCreateEmbedding` calls. * Dockerfile in the `examples/server` sample app which facilitates the easy creation of a containerized ready-to-run sample app for situations where that makes testing easier. ### Fixed * `.Ignore` was not ignoring transaction. Fixes [Issue #845](https://github.com/newrelic/go-agent/issues/845). * Added nil error check in wrap function. Fixes [Issue #862](https://github.com/newrelic/go-agent/issues/862). * `WrapBackgroundCore` background logger was not sending logs to New Relic. Fixes [Issue #859](https://github.com/newrelic/go-agent/issues/859). * Corrected pgx5 integration example which caused a race condition. Thanks to @WillAbides! Fixes [Issue #855](https://github.com/newrelic/go-agent/issues/855). * Updated third-party library versions due to reported security or other supportability issues: * `github.com/jackc/pgx/v5` to 5.5.4 in `nrpgx5` integration * `google.gopang.org/protobuf` to 1.33.0 in `nrmicro` and `nrgrpc` integrations * `github.com/jackc/pgx/v4` to 4.18.2 in `nrpgx` integration ### AI Monitoring Configuration New configuration options are available specific to AI monitoring. These settings include: * `AIMonitoring.Enabled`, configured via `ConfigAIMonitoring.Enabled(`_bool_`)` [default `false`] * `AIMonitoring.Streaming.Enabled`, configured via `ConfigAIMonitoringStreamingEnabled(`_bool_`)` [default `true`] * `AIMonitoring.Content.Enabled`, configured via `ConfigAIMonitoringContentEnabled(`_bool_`)` [default `true`] ### AI Monitoring Public API Methods Two new AI monitoring related public API methods have been added, as methods of the `newrelic.Application` value returned by `newrelic.NewApplication`: * [app.RecordLLMFeedbackEvent](https://pkg.go.dev/github.com/newrelic/go-agent/v3/newrelic#Application.RecordLLMFeedbackEvent) * [app.SetLLMTokenCountCallback](https://pkg.go.dev/github.com/newrelic/go-agent/v3/newrelic#Application.SetLLMTokenCountCallback) ### AI Monitoring New Relic AI monitoring is the industry’s first APM solution that provides end-to-end visibility for AI Large Language Model (LLM) applications. It enables end-to-end visibility into the key components of an AI LLM application. With AI monitoring, users can monitor, alert, and debug AI-powered applications for reliability, latency, performance, security and cost. AI monitoring also enables AI/LLM specific insights (metrics, events, logs and traces) which can easily integrate to build advanced guardrails for enterprise security, privacy and compliance. AI monitoring offers custom-built insights and tracing for the complete lifecycle of an LLM’s prompts and responses, from raw user input to repaired/polished responses. AI monitoring provides built-in integrations with popular LLMs and components of the AI development stack. This release provides instrumentation for [OpenAI](https://pkg.go.dev/github.com/newrelic/go-agent/v3/integrations/nropenai) and [Bedrock](https://pkg.go.dev/github.com/newrelic/go-agent/v3/integrations/nrawsbedrock). When AI monitoring is enabled with `ConfigAIMonitoringEnabled(true)`, the agent will now capture AI LLM related data. This data will be visible under a new APM tab called AI Responses. See our [AI Monitoring documentation](https://docs.newrelic.com/docs/ai-monitoring/intro-to-ai-monitoring/) for more details. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.30.0 ### Added * Updated the depencency on nrsecurityagent to 1.0.0. * Added new integration, logcontext-v2/nrslog, which instruments logging via the new slog library. ### Fixed * Redacts license keys from error reporting. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.29.1 ### Added * Added Dockerized Unit Tests for Github Actions (internal build support) ### Fixes * Updated version of New Relic Security Agent (enables bug fixes released in that agent code for use with the Go Agent). ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.29.0 ### Added * Security agent integration `nrsecurityagent` now reports security configuraiton information along with the overall Go Agent configuration values. (Updates `nrsecurityagent` to v1.2.0.) * Code-Level Metrics collection efficiency enhancement allows user callback function for as-needed (and just-in-time) evaluation of custom code locations rather than up-front location overrides, via the `WithCodeLocationCallback` CLM option. Deprecates `WithCodeLocation` option (although the latter function is still supported for compatibility with existing code). * Added extended synthetics support for new `X-Newrelic-Synthetics-Info` HTTP headers. * Documentation fixes. * Removed deprecated `ROADMAP.md` file. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.28.1 ### Added Added Supportability Metrics to `nrfasthttp` (brings `nrfasthttp` version to v1.0.1). Always Link Transaction IDs to traces regardless of whether Distributed Tracing is enabled or not ### Fixed Fixed an issue where `nil` `Request.Body` could be set to non-`nil` `request.Body` with zero length when the security agent is enabled ### Security More Secure URL Redaction ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.28.0 ### Fixed * Bumped gRPC from 1.54.0 -> 1.56.3 in the following packages /v3/integrations/nrgrpc, /v3/, /v3/integrations/nrgrpc * Bumped golang.org/x/net from 0.8.0 -> 0.17.0 in package /v3/integrations/nrgraphqlgo * Fixed issue where nrfasthttp would not properly register security agent headers * Move fasthttp instrumentation into a new integration package, nrfasthttp * Fixed issue where usage of io.ReadAll() was causing a memory leak ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.27.0 ### Added * Added Support for getting Container ID's from cgroup v2 docker containers * A new instrumentation package for RabbitMQ with distributed tracing support: nramqp ### Fixed * Unit tests repairs and improvements * Removed deprecated V2 code from the repository. The support timeframe for this code has expired and is no longer recommended for use. * Bumped github.com/graphql-go/graphql from 0.7.9 to 0.8.1 ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.26.0 ### Added * Extended implementation of the `nrpgx5` integration (now v1.2.0). This instruments Postgres database operations using the `jackc/pgx/v5` library, including the direct access mode of operation as opposed to requiring code to use the library compatibly with the standard `database/sql` library. ### Corrections * See below for revised release notes for the 3.25.1 and the retracted 3.25.0 releases. We have clarified what was released at those versions; see also the revised notes for 3.22.0 and 3.22.1 for the same reason. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.25.1 ### Added * Added Support for FastHTTP package * Added newrelic.WrapHandleFuncFastHTTP() and newrelic.StartExternalSegmentFastHTTP() functions to instrument fasthttp context and create wrapped handlers. These functions work similarly to the existing ones for net/http * Added client-fasthttp and server-fasthttp examples to help get started with FastHTTP integration ### Fixed * Corrected a bug where the security agent failed to correctly parse the `NEW_RELIC_SECURITY_AGENT_ENABLED` environment variable. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.25.0 (retracted) This release was retracted due to an error in the release process which caused the wrong git commit to be tagged. Since the erroneous `v3.25.0` tag was already visible publicly and may already have been picked up by the Go language infrastructure, we retracted the incorrect 3.25.0 version and released the changes intended for 3.25.0 as version 3.25.1, so users of the Go Agent library will reliably get the correct code. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves (i.e., Go versions 1.19 and later are supported). We recommend updating to the latest agent version as soon as it’s available. If you can’t upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about keeping agents up to date. (https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent/) See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.24.1 ### Fixed * Performance improvement around calls to security agent. In some cases, unnecessary setup operations were being performed even if there was no security agent present to use that. These are now conditional on the security agent being present in the application (note that this will enable the setup code if the security agent is *present* in the application, regardless of whether it's currently enabled to run). This affects: * Base agent code (updated to v3.24.1) * `nrmongo` integration (updated to v1.1.1) * Resolved a race condition caused by the above-mentioned calls to the security agent. * Fixed unit tests for integrations which were failing because code level metrics are enabled by default now: * `nrawssdk-v1` (updated to v1.1.2) * `nrawssdk-v2` (updated to v1.2.2) * `nrecho-v3` (updated to v1.0.2) * `nrecho-v4` (updated to v1.0.4) * `nrhttprouter` (updated to v1.0.2) * `nrlambda` (updated to v1.2.2) * `nrnats` (updated to v1.1.5) * `nrredis-v8` (updated to v1.0.1) ### Changed * Updated all integration `go.mod` files to reflect supported Go language versions. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves (i.e., Go versions 1.19 and later are supported). We recommend updating to the latest agent version as soon as it's available. If you can't upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about keeping agents up to date. (https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent/) See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.24.0 ### Added * Turned Code Level Metrics on by default * Added new test case to check if the nrsecurityagent is enabled in the gRPC integration * Added new test case for InfoInterceptorStatusHandler function in the gRPC integration * Added Name() method for Transaction values to get the current transaction name. ### Fixed * Bumped gin from 1.9.0 to 1.9.1 * Bumped gosnowflake from 1.6.16 to 1.6.19 * Bumped nrsecurityagent to 1.1.0 with improved reporting of gRPC protocol versions. * Fixed a bug where expected errors weren't being properly marked as expected on new relic dashboards ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves (i.e., Go versions 1.19 and later are supported). We recommend updating to the latest agent version as soon as it's available. If you can't upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about keeping agents up to date. (https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent/) See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.23.1 ## Added * Added newrelic.ConfigDatastoreRawQuery(true) configuration option to allow raw SQL queries to appear in new relic dashboards * Added license file to nrsecurityagent integration * Added enriched serverless debug logging for faster debugging ## Fixed * Removed timeouts on two tests in trace_observer_test.go * Bumped nrnats test to go1.19 * Bumped graphql-go to v1.3.0 in the nrgraphgophers integration We recommend updating to the latest agent version as soon as it's available. If you can't upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about keeping agents up to date. (https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent/) See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.23.0 ### Added * Adds the `nrsecurityagent` integration for performing Interactive Application Security Testing (IAST) of your application. * This action increments the version numbers of the following integrations: * `nrgin` v1.2.0 * `nrgrpc` v1.4.0 * `nrmicro` v1.2.0 * `nrmongo` v1.2.0 * `nrsqlite3` v1.2.0 To learn how to use IAST with the New Relic Go Agent, [check out our documentation](https://docs.newrelic.com/docs/iast/use-iast/). ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves (i.e., Go versions 1.19 and later are supported). See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.22.1 ### Added * New Apache Kafka integration nrsarama that instruments the Sarama library https://github.com/Shopify/sarama * New logs in context integration logcontext-v2/nrzap that instruments the zap logging framework https://github.com/uber-go/zap * Integration tests created for the nrlogrus and nrzapintegrations * Updated integration tests for nrlogxi ### Security Fixes * Bumped sys package to v0.1.0 in the nrmssql integration * Bumped net package to v0.7.0 in the nrgrpc, nrmssql , and nrnats integrations * Bumped aws-sdk-go package to v1.34.0 in the nrawssdk-v1 integration * Bumped text package to v0.3.8 in the nrnats, and nrpgx integrations * Bumped gin package to v1.9.0 in the nrgin integration * Bumped crypto package to v0.1.0 in the nrpgx integration * Fixed integration tests in nrnats package not correctly showing code coverage * Corrects an error in the release process for 3.22.0. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.22.0 (retracted) This release has been retracted due to an error in the release process which caused it to be incorrectly created. Instead, release 3.22.1 was issued with the changes intended for 3.22.0. ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go agent and third-party components. ## 3.21.1 ### Added * nrredis-v9: automatic instrumentation for Go redis v9 ### Fixed * Agent now requires Go version 1.18 or higher. * Removed support for Go version 1.17. This version of Go is outside of the support window. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.21.0 ### Added * New Errors inbox features: * User tracking: You can now see the number of users impacted by an error group. Identify the end user with the setUser method. * Error fingerprint: Are your error occurrences grouped poorly? Set your own error fingerprint via a callback function. * Ability to disable reporting parameterized query in nrpgx-5 ### Fixed * Improved test coverage for gRPC integration, nrgrpc ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.20.4 ### Fixed * nrmssql driver updated to use version maintained by Microsoft * bug where error messages were not truncated to the maximum size, and would get dropped if they were too large * bug where number of span events was hard coded to 1000, and config setting was being ignored ### Added * improved performance of ignore error code checks in agent * HTTP error codes can be set as expected by adding them to ErrorCollector.ExpectStatusCodes in the config ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.20.3 Please note that the v2 go agent is no longer supported according to our EOL policy. ### Fixed * Performance Improvements for compression * nrsnowflake updated to golang 1.17 versions of packages ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.20.2 ### Added * New `NoticeExpectedError()` method allows you to capture errors that you are expecting to handle, without triggering alerts ### Fixed * More defensive harvest cycle code that will avoid crashing even in the event of a panic. * Update `nats-server` version to avoid known zip-slip exploit * Update `labstack/echo` version to mitigate known open redirect exploit ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.20.1 ### Added * New integration `nrpgx5` v1.0.0 to instrument `github.com/jackc/pgx/v5`. ### Changed * Changed the following `TraceOption` function to be consistent with their usage and other related identifier names. The old names remain for backward compatibility, but new code should use the new names. * `WithIgnoredPrefix` -> `WithIgnoredPrefixes` * `WithPathPrefix` -> `WithPathPrefixes` * Implemented better handling of Code Level Metrics reporting when the data (e.g., function names) are excessively long, so that those attributes are suppressed rather than being reported with truncated names. Specifically: * Attributes with values longer than 255 characters are dropped. * No CLM attributes at all will be attached to a trace if the `code.function` attribute is empty or is longer than 255 characters. * No CLM attributes at all will be attached to a trace if both `code.namespace` and `code.filepath` are longer than 255 characters. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.20.0 **PLEASE READ** these changes, and verify your config settings to ensure your application behaves how you intend it to. This release changes some default behaviors in the go agent. ### Added * The Module Dependency Metrics feature was added. This collects the list of modules imported into your application, to aid in management of your application dependencies, enabling easier vulnerability detection and response, etc. * This feature is enabled by default, but may be disabled by explicitly including `ConfigModuleDependencyMetricsEnable(false)` in your application, or setting the equivalent environment variable or `Config` field direclty. * Modules may be explicitly excluded from the report via the `ConfigModuleDependencyMetricsIgnoredPrefixes` option. * Excluded module names may be redacted via the `ConfigModuleDependencyMetricsRedactIgnoredPrefixes` option. This is enabled by default. * Application Log Forwarding will now be **ENABLED** by default * Automatic application log forwarding is now enabled by default. This means that logging frameworks wrapped with one of the [logcontext-v2 integrations](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-compatibility-requirements/) will automatically send enriched application logs to New Relic with this version of the agent. To learn more about this feature, see the [APM logs in context documentation](https://docs.newrelic.com/docs/logs/logs-context/logs-in-context/). For additional configuration options, see the [Go logs in context documentation](https://docs.newrelic.com/docs/logs/logs-context/configure-logs-context-go). To learn about how to toggle log ingestion on or off by account, see our documentation to [disable automatic](https://docs.newrelic.com/docs/logs/logs-context/disable-automatic-logging) logging via the UI or API. * If you are using a logcontext-v2 extension, but don't want the agent to automatically forward logs, please configure `ConfigAppLogForwardingEnabled(false)` in your application. * Environment variables have been added for all application logging config options: * `NEW_RELIC_APPLICATION_LOGGING_ENABLED` * `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED` * `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED` * `NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED` * `NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED` * Custom Event Limit Increase * This version increases the **DEFAULT** limit of custom events from 10,000 events per minute to 30,000 events per minute. In the scenario that custom events were being limited, this change will allow more custom events to be sent to New Relic. There is also a new configurable **MAXIMUM** limit of 100,000 events per minute. To change the limits, set `ConfigCustomInsightsEventsMaxSamplesStored(limit)` to the limit you want in your application. To learn more about the change and how to determine if custom events are being dropped, see our Explorers Hub [post](https://discuss.newrelic.com/t/send-more-custom-events-with-the-latest-apm-agents/190497). * New config option `ConfigCustomInsightsEventsEnabled(false)` can be used to disable the collection of custom events in your application. ### Changed * Changed the following names to be consistent with their usage and other related identifier names. The old names remain for backward compatibility, but new code should use the new names. * `ConfigCodeLevelMetricsIgnoredPrefix` -> `ConfigCodeLevelMetricsIgnoredPrefixes` * `ConfigCodeLevelMetricsPathPrefix` -> `ConfigCodeLevelMetricsPathPrefixes` * `NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX` -> `NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIXES` * `NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX` -> `NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIXES` * When excluding information reported from CodeLevelMetrics via the `IgnoredPrefixes` or `PathPrefixes` configuration fields (e.g., by specifying `ConfigCodeLevelMetricsIgnoredPrefixes` or `ConfigCodeLevelMetricsPathPrefixes`), the names of the ignored prefixes and the configured path prefixes may now be redacted from the agent configuration information sent to New Relic. * This redaction is enabled by default, but may be disabled by supplying a `false` value to `ConfigCodeLevelMetricsRedactPathPrefixes` or `ConfigCodeLevelMetricsRedactIgnoredPrefixes`, or by setting the corresponding `Config` fields or environment variables to `false`. ### Fixed * [#583](https://github.com/newrelic/go-agent/issues/583): fixed a bug in zerologWriter where comma separated fields in log message confused the JSON parser and could cause panics. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.19.2 ### Changed * Updated nrgin integration to more accurately report code locations when code level metrics are enabled. * The Go Agent and all integrations now require Go version 1.17 or later. * Updated minimum versions for third-party modules. * nrawssdk-v2, nrecho-v4, nrgrpc, nrmongo, nrmysql, nrnats, and nrstan now require Go Agent 3.18.2 or later * the Go Agent now requires protobuf 1.5.2 and grpc 1.49.0 * Internal dev process and unit test improvements. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.19.1 - Hotfix Release ### Changed * Moved the v3/internal/logcontext/nrwriter module to v3/integrations/logcontext-v2/nrwriter ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.19.0 ### Added * `logcontext-v2/logWriter` plugin: a new logs in context plugin that supports the standard library logging package. * `logcontext-v2/zerologWriter` plugin: a new logs in context plugin for zerolog that will replace the old logcontext-v2/zerolog plugin. This plugin is more robust, and will be able to support a richer set of features than the previous plugin. * see the updated [logs in context documentation](https://docs.newrelic.com/docs/logs/logs-context/configure-logs-context-go) for information about configuration and installation. ### Changed * the logcontext-v2/zerolog plugin will be deprecated once the 3.17.0 release EOLs. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.18.2 ### Added * Added `WithDefaultFunctionLocation` trace option. This allows the caller to indicate a fall-back function to use for CLM in case no other location was found first. * Added caching versions of the code-level metrics functions `ThisCodeLocation` and `FunctionLocation` , and trace options `WithThisCodeLocation` and `WithFunctionLocation`. These improve performance by caching the result of computing the source code location, and reuse that cached result on all subsequent calls. * Added a `WithCodeLevelMetrics` trace option to force the collection of CLM data even if it would have been excluded as being out of the configured scope. (Note that CLM data are _never_ collected if CLM is turned off globally or if the `WithoutCodeLevelMetrics` option was specified for the same transaction.) * Added an exported `CodeLevelMetricsScopeLabelToValue` function to convert a list of strings describing CLM scopes in the same manner as the `NEW_RELIC_CODE_LEVEL_METRICS_SCOPE` environment variable (but as individual string parameters), returning the `CodeLevelMetricsScope` value which corresponds to that set of scopes. * Added a new `CodeLevelMetricsScopeLabelListToValue` function which takes a comma-separated list of scope names exactly as the `NEW_RELIC_CODE_LEVEL_METRICS_SCOPE` environment variable does, and returns the `CodeLevelMetrics` value corresponding to that set of scopes. * Added text marshaling and unmarshaling for the `CodeLevelMetricsScope` value, allowing the `CodeLevelMetrics` field of the configuration `struct` to be converted to or from JSON or other text-based encoding representations. ### Changed * The `WithPathPrefix` trace option now takes any number of `string` parameters, allowing multiple path prefixes to be recognized rather than just one. * The `FunctionLocation` function now accepts any number of function values instead of just a single one. The first such parameter which indicates a valid function, and for which CLM data are successfully obtained, is the one which will be reported. * The configuration `struct` field `PathPrefix` is now deprecated with the introduction of a new `PathPrefixes` field. This allows for multiple path prefixes to be given to the agent instead of only a single one. * The `NEW_RELIC_CODE_LEVEL_METRICS_SCOPE` environment variable now accepts a comma-separated list of pathnames. ### Fixed * Improved the implementation of CLM internals to improve speed, robustness, and thread safety. * Corrected the implementation of the `WrapHandle` and `WrapHandleFunc` functions so that they consistently report the function being invoked by the `http` framework, and improved them to use the new caching functions and ensured they are thread-safe. This release fixes [issue #557](https://github.com/newrelic/go-agent/issues/557). ### Compatibility Notice As of release 3.18.0, the API was extended by allowing custom options to be added to calls to the `Application.StartTransaction` method and the `WrapHandle` and `WrapHandleFunc` functions. They are implemented as variadic functions such that the new option parameters are optional (i.e., zero or more options may be added to the end of the function calls) to be backward-compatible with pre-3.18.0 usage of those functions. This prevents the changes from breaking existing code for typical usage of the agent. However, it does mean those functions' call signatures have changed: * `StartTransaction(string)` -> `StartTransaction(string, ...TraceOption)` * `WrapHandle(*Application, string, http.Handler)` -> `WrapHandle(*Application, string, http.Handler, ...TraceOption)` * `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request))` -> `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request), ...TraceOption)` If, for example, you created your own custom interface type which includes the `StartTransaction` method or something that depends on these functions' exact call semantics, that code will need to be updated accordingly before using version 3.18.0 (or later) of the Go Agent. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.18.1 ### Added * Extended the `IgnoredPrefix` configuration value for Code-Level Metrics so that multiple such prefixes may be given instead of a single one. This deprecates the `IgnoredPrefix` configuration field of `Config.CodeLevelMetrics` in favor of a new slice field `IgnoredPrefixes`. The corresponding configuration option-setting functions `ConfigCodeLevelMetricsIgnoredPrefix` and `WithIgnoredPrefix` now take any number of string parameters to set these values. Since those functions used to take a single string value, this change is backward-compatible with pre-3.18.1 code. Accordingly, the `NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX` environment variable is now a comma-separated list of prefixes. Fixes [Issue #551](https://github.com/newrelic/go-agent/issues/551). ### Fixed * Corrected some small errors in documentation of package features. Fixes [Issue #550](https://github.com/newrelic/go-agent/issues/550) ### Compatibility Notice As of release 3.18.0, the API was extended by allowing custom options to be added to calls to the `Application.StartTransaction` method and the `WrapHandle` and `WrapHandleFunc` functions. They are implemented as variadic functions such that the new option parameters are optional (i.e., zero or more options may be added to the end of the function calls) to be backward-compatible with pre-3.18.0 usage of those functions. This prevents the changes from breaking existing code for typical usage of the agent. However, it does mean those functions' call signatures have changed: * `StartTransaction(string)` -> `StartTransaction(string, ...TraceOption)` * `WrapHandle(*Application, string, http.Handler)` -> `WrapHandle(*Application, string, http.Handler, ...TraceOption)` * `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request))` -> `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request), ...TraceOption)` If, for example, you created your own custom interface type which includes the `StartTransaction` method or something that depends on these functions' exact call semantics, that code will need to be updated accordingly before using version 3.18.0 (or later) of the Go Agent. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. ## 3.18.0 ### Added * Code-Level Metrics are now available for instrumented transactions. This is off by default but once enabled via `ConfigCodeLevelMetricsEnabled(true)` transactions will include information about the location in the source code where `StartTransaction` was invoked. * Adds information about where in your source code transaction traces originated. * See the Go Agent documentation for details on [configuring](https://docs.newrelic.com/docs/apm/agents/go-agent/configuration/go-agent-code-level-metrics-config) Code-Level Metrics and how to [instrument](https://docs.newrelic.com/docs/apm/agents/go-agent/instrumentation/go-agent-code-level-metrics-instrument) your code using them. * New V2 logs in context plugin is available for Logrus, packed with all the features you didn't know you wanted: * Automatic Log Forwarding * Log Metrics * Capture logs anywhere in your code; both inside or outside of a transaction. * Use the Logrus formatting package of your choice * Local Log Decorating is now available for the new logcontext-v2/nrlogrus plugin only. This is off by default but can be enabled with `ConfigAppLogForwardingEnabled(true)`. ### Fixed * Fixed issue with custom event limits and number of DT Spans to more accurately follow configured limits. ### Compatibility Notice This release extends the API by allowing custom options to be added to calls to the `Application.StartTransaction` method and the `WrapHandle` and `WrapHandleFunc` functions. They are implemented as variadic functions such that the new option parameters are optional (i.e., zero or more options may be added to the end of the function calls) to be backward-compatible with pre-3.18.0 usage of those functions. This prevents the changes from breaking existing code for typical usage of the agent. However, it does mean those functions' call signatures have changed: * `StartTransaction(string)` -> `StartTransaction(string, ...TraceOption)` * `WrapHandle(*Application, string, http.Handler)` -> `WrapHandle(*Application, string, http.Handler, ...TraceOption)` * `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request))` -> `WrapHandleFunc(*Application, string, func(http.ResponseWriter, *http.Request), ...TraceOption)` If, for example, you created your own custom interface type which includes the `StartTransaction` method or something that depends on these functions' exact call semantics, that code will need to be updated accordingly before using version 3.18.0 of the Go Agent. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. * Note that the oldest supported version of the Go Agent is 3.6.0. ## 3.17.0 ### Added * Logs in context now supported for zerolog. * This is a quick way to view logs no matter where you are in the platform. * Adds support for logging metrics which shows the rate of log messages by severity in the Logs chart in the APM Summary view. This is enabled by default in this release. * Adds support for forwarding application logs to New Relic. This automatically sends application logs that have been enriched to power APM logs in context. This is disabled by default in this release. This will be on by default in a future release. * To learn more about APM logs in context see the documentation [here](https://docs.newrelic.com/docs/logs/logs-context/logs-in-context). * Includes the `RecordLog` function for recording log data from a single log entry * An integrated plugin for zerolog to automatically ingest log data with the Go Agent. * Resolves [issue 178](https://github.com/newrelic/go-agent/issues/178), [issue 488](https://github.com/newrelic/go-agent/issues/488), [issue 489](https://github.com/newrelic/go-agent/issues/489), [issue 490](https://github.com/newrelic/go-agent/issues/490), and [issue 491](https://github.com/newrelic/go-agent/issues/491) . * Added integration for MS SQL Server ([PR 425](https://github.com/newrelic/go-agent/pull/425); thanks @ishahid91!) * This introduces the `nrmssql` integration v1.0.0. * Added config function `ConfigCustomInsightsEventsMaxSamplesStored` for limiting the number of samples stored in a custom insights event. Fixes [issue 476](https://github.com/newrelic/go-agent/issues/476) ### Fixed * Improved speed of building distributed trace header JSON payload. Fixes [issue 505](https://github.com/newrelic/go-agent/issues/505). * Renamed the gRPC attribute names from `GrpcStatusLevel`, `GrpcStatusMessage`, and `GrpcStatusCode` to `grpcStatusLevel`, `grpcStatusMessage`, and `grpcStatusCode` respectively, to conform to existing naming conventions for New Relic agents. Fixes [issue 492](https://github.com/newrelic/go-agent/issues/492). * Updated `go.mod` for the `nrgin` integration to mitigate security issue in 3rd party dependency. * Updated `go.mod` for the `nrawssdk-v1` integration to properly reflect its dependency on version 3.16.0 of the Go Agent. * Updated `go.mod` for the `nrlambda` integration to require `aws-lambda-go` version 1.20.0. ([PR 356](https://github.com/newrelic/go-agent/pull/356); thanks MattWhelan!) ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. * Note that the oldest supported version of the Go Agent is 3.6.0. # ChangeLog ## 3.16.1 ### Fixed * Changed dependency on gRPC from v1.27.0 to v1.39.0. This in turn changes gRPC's dependency on `x/crypto` to v0.0.0-20200622213623-75b288015ac9, which fixes a security vulnerability in the `x/crypto` standard library module. Fixes [issue #451](https://github.com/newrelic/go-agent/issues/451). * Incremented version number of the `nrawssdk-v1` integration from v1.0.1 to v1.1.0 to resolve an incompatibility issue due to changes to underlying code. Fixes [issue #499](https://github.com/newrelic/go-agent/issues/499) ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.16.0 ### Added * Distributed Tracing is now the default mode of operation. It may be disabled by user configuration if so desired. [PR #495](https://github.com/newrelic/go-agent/pull/495) * To disable DT, add `newrelic.ConfigDistributedTracerEnabled(false)` to your application configuration. * To change the reservoir limit for how many span events are to be collected per harvest cycle from the default, add `newrelic.ConfigDistributedTracerReservoirLimit(`*newlimit*`)` to your application configuration. * The reservoir limit's default was increased from 1000 to 2000. * The maximum reservoir limit supported is 10,000. * Note that Cross Application Tracing is now deprecated. * Added support for gathering memory statistics via `PhysicalMemoryBytes` functions for OpenBSD. ### Fixed * Corrected some example code to be cleaner. * Updated version of nats-streaming-server. [PR #458](https://github.com/newrelic/go-agent/pull/458) * Correction to nrpkgerrors so that `nrpkgerrors.Wrap` now checks if the error it is passed has attributes, and if it does, copies them into the New Relic error it creates. This fixes [issue #409](https://github.com/newrelic/go-agent/issues/409) via [PR #441](https://github.com/newrelic/go-agent/pull/441). * This increments the `nrpkgerrors` version to v1.1.0. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.15.2 ### Added * Strings logged via the Go Agent's built-in logger will have strings of the form `license_key=`*hex-string* changed to `license_key=[redacted]` before they are output, regardless of severity level, where *hex-string* means a sequence of upper- or lower-case hexadecimal digits and dots ('.'). This incorporates [PR #415](https://github.com/newrelic/go-agent/pull/415). ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.15.1 ### Fixed * Updated support for SQL database instrumentation across the board for the Go Agent’s database integrations to more accurately extract the database table name from SQL queries. Fixes [Issue #397](https://github.com/newrelic/go-agent/issues/397). * Updated the `go.mod` file in the `nrecho-v4` integration to require version 4.5.0 of the `github.com/labstack/echo` package. This addresses a security concern arising from downstream dependencies in older versions of the echo package, as described in the [release notes](https://github.com/labstack/echo/releases/tag/v4.5.0) for `echo` v4.5.0. ### ARM64 Compatibility Note The New Relic Go Agent is implemented in platform-independent Go, and supports (among the other platforms which run Go) ARM64/Graviton2 using Go 1.17+. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.15.0 ### Fixed * Updated mongodb driver version to 1.5.1 to fix security issue in external dependency. Fixes [Issue #358](https://github.com/newrelic/go-agent/issues/358) and [Issue #370](https://github.com/newrelic/go-agent/pull/370). * Updated the `go.mod` file in the `nrgin` integration to require version 1.7.0 of the `github.com/gin-gonic/gin` package. This addresses [CVE-2020-28483](https://github.com/advisories/GHSA-h395-qcrw-5vmq) which documents a vulnerability in versions of `github.com/gin-gonic/gin` earlier than 1.7.0. ### Added * New integration `nrpgx` added to provide the same functionality for instrumenting Postgres database queries as the existing `nrpq` integration, but using the [pgx](https://github.com/jackc/pgx) driver instead. This only covers (at present) the use case of the `pgx` driver with the standard library `database/sql`. Fixes [Issue #142](https://github.com/newrelic/go-agent/issues/142) and [Issue #292](https://github.com/newrelic/go-agent/issues/292) ### Changed * Enhanced debugging logs so that New Relic license keys are redacted from the log output. Fixes [Issue #353](https://github.com/newrelic/go-agent/issues/353). * Updated the advice in `GUIDE.md` to have correct `go get` commands with explicit reference to `v3`. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.14.1 ### Fixed * A typographical error in the nrgrpc unit tests was fixed. Fixes [Issue #344](https://github.com/newrelic/go-agent/issues/344). This updates the nrgrpc integration to version 1.3.1. ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.14.0 ### Fixed * Integration tags and `go.mod` files for integrations were updated so that [pkg.go.dev]() displays the documentation for each integration correctly. * The `nrgrpc` server integration was reporting all non-`OK` grpc statuses as errors. This has now been changed so that only selected grpc status codes will be reported as errors. Others are shown (via transaction attributes) as "warnings" or "informational" messages. There is a built-in set of defaults as to which status codes are reported at which severity levels, but this may be overridden by the caller as desired. Also supports custom grpc error handling functions supplied by the user. * This is implemented by adding `WithStatusHandler()` options to the end of the `UnaryServerInterceptor()` and `StreamServerInterceptor()` calls, thus extending the capability of those functions while retaining the existing functionality and usage syntax for backward compatibility. * Added advice on the recommended usage of the `app.WaitForConnection()` method. Fixes [Issue #296](https://github.com/newrelic/go-agent/issues/296) ### Added * Added a convenience function to build distributed trace header set from a JSON string for use with the `AcceptDistributedTraceHeaders()` method. Normally, you must create a valid set of HTTP headers representing the trace identification information from the other trace so the new trace will be associated with it. This needs to be in a Go `http.Header` type value. * If working only in Go, this may be just fine as it is. However, if the other trace information came from another source, possibly in a different language or environment, it is often the case that the trace data is already presented to you in the form of a JSON string. * This new function, `DistributedTraceHeadersFromJSON()`, creates the required `http.Header` value from the JSON string without requiring manual effort on your part. * We also provide a new all-in-one method `AcceptDistributedTraceHeadersFromJSON()` to be used in place of `AcceptDistributedTraceHeaders()`. It accepts a JSON string rather than an `http.Header`, adding its trace info to the new transaction in one step. * Fixes [Issue #331](https://github.com/newrelic/go-agent/issues/331) ### Changed * Improved the NR AWS SDK V2 integration to use the current transaction rather than the one passed in during middleware creation, if `nil` is passed into nrawssdk-v2.AppendMiddlewares. Thanks to @HenriBeck for noticing and suggesting improvement, and thanks to @nc-wittj for the fantastic PR! [#328](https://github.com/newrelic/go-agent/pull/328) ### Support Statement New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. ## 3.13.0 ### Fixed * Replaced the NR AWS SDK V2 integration for the v3 agent with a new version that works. See the v3/integrations/nrawssdk-v2/example/main.go file for an example of how to use it. Issues [#250](https://github.com/newrelic/go-agent/issues/250) and [#288](https://github.com/newrelic/go-agent/issues/288) are fixed by this PR. [#309](https://github.com/newrelic/go-agent/pull/309) * Fixes issue [#221](https://github.com/newrelic/go-agent/issues/221): grpc errors reported in code watched by `UnaryServerInterceptor()` or `StreamServerInterceptor()` now create error events which are reported to the UI with the error message string included. [#317](https://github.com/newrelic/go-agent/pull/317) * Fixes documentation in `GUIDE.md` for `txn.StartExternalSegment()` to reflect the v3 usage. Thanks to @abeltay for calling this to our attention and submitting PR [#320](https://github.com/newrelic/go-agent/pull/320). ### Changes * The v3/examples/server/main.go example now uses `newrelic.ConfigFromEnvironment()`, rather than explicitly pulling in the license key with `newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY"))`. The team is starting to use this as a general systems integration testing script, and this facilitates testing with different settings enabled. ### Support Statement * New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach [end-of-life](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/install-configure/notification-changes-new-relic-saas-features-distributed-software). ## 3.12.0 ### Changes * Updated `CHANGELOG.md` release notes language, to correct typographical errors and clean up grammar. [#289](https://github.com/newrelic/go-agent/issues/289) ### Fixed * When using DAX to query a dynamodb table, the New Relic instrumentation panics with a `nil dereference` error. This was due to the way that the request is made internally such that there is no `HTTPRequest.Header` defined, but one was expected. This correction checks for the existence of that header and takes an appropriate course of action if one is not found. [#287](https://github.com/newrelic/go-agent/issues/287) Thanks to @odannyc for reporting the issue and providing a pull request with a suggested fix. ### Support Statement * New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach [end-of-life](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/install-configure/notification-changes-new-relic-saas-features-distributed-software). ## 3.11.0 ### New Features * Aerospike is now included on the list of recognized datastore names. Thanks @vkartik97 for your PR! [#233](https://github.com/newrelic/go-agent/pull/233) * Added support for verison 8 of go-redis. Thanks @ilmimris for adding this instrumentation! [#251](https://github.com/newrelic/go-agent/pull/251) ### Changes * Changed logging level for messages resulting from Infinite Tracing load balancing operations. These were previously logged as errors, and now they are debugging messages. [#276](https://github.com/newrelic/go-agent/pull/276) ### Fixed * When the agent is configured with `cfg.ErrorCollector.RecordPanics` set to `true`, panics would be recorded by New Relic, but stack traces would not be logged as the Go Runtime usually does. The agent now logs stack traces from within its panic handler, providing similar functionality. [#278](https://github.com/newrelic/go-agent/pull/278) * Added license files to some integrations packages to ensure compatibility with package.go.dev. Now the documentation for our integrations shows up again on go.docs. ### Support statement * New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach [end-of-life](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/install-configure/notification-changes-new-relic-saas-features-distributed-software). ## 3.10.0 ### New Features * To keep up with the latest security protocols implemented by Amazon Web Services, the agent now uses [AWS IMDSv2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) to find utilization data. [#249](https://github.com/newrelic/go-agent/pull/249) ### Changes * Updated the locations of our license files so that Go docs https://pkg.go.dev will display our agent. Thanks @tydavis for your PR to fix this! [#254](https://github.com/newrelic/go-agent/pull/254) * Added an Open Source repo linter GitHub action that runs on push. [#262](https://github.com/newrelic/go-agent/pull/262) * Updated the README.md file to correctly show the support resources from New Relic. [#255](https://github.com/newrelic/go-agent/pull/255) ### Support statement * New Relic recommends that you upgrade the agent regularly to ensure that you're getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach [end-of-life](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/install-configure/notification-changes-new-relic-saas-features-distributed-software). ## 3.9.0 ### Changes * When sending Serverless telemetry using the `nrlambda` integration, support an externally-managed named pipe. ## 3.8.1 ### Bug Fixes * Fixed an issue that could cause orphaned Distributed Trace spans when using SQL instrumentation like `nrmysql`. ## 3.8.0 ### Changes * When marking a transaction as a web transaction using [Transaction.SetWebRequest](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebRequest), it is now possible to include a `Host` field in the [WebRequest](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WebRequest) struct, which defaults to the empty string. ### Bug Fixes * The `Host` header is now being correctly captured and recorded in the `request.headers.host` attribute, as described [here](https://docs.newrelic.com/docs/agents/go-agent/instrumentation/go-agent-attributes#requestHeadersHost). * Previously, the timestamps on Spans and Transactions were being written using different data types, which sometimes caused rounding errors that could cause spans to be offset incorrectly in the UI. This has been fixed. ## 3.7.0 ### Changes * When `Config.Transport` is nil, no longer use the `http.DefaultTransport` when communicating with the New Relic backend. This addresses an issue with shared transports as described in https://github.com/golang/go/issues/33006. * If a timeout occurs when attempting to send data to the New Relic backend, instead of dropping the data, we save it and attempt to send it with the next harvest. Note data retention limits still apply and the agent will still start to drop data when these limits are reached. We attempt to keep the highest priority events and traces. ## 3.6.0 ### New Features * Added support for [adding custom attributes directly to spans](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Segment.AddAttribute). These attributes will be visible when looking at spans in the Distributed Tracing UI. Example: ```go txn := newrelic.FromContext(r.Context()) sgmt := txn.StartSegment("segment1") defer sgmt.End() sgmt.AddAttribute("mySpanString", "hello") sgmt.AddAttribute("mySpanInt", 123) ``` * Custom attributes added to the transaction with `txn.AddAttribute` are now also added to the root Span Event and will be visible when looking at the span in the Distributed Tracing UI. These custom attributes can be disabled from all destinations using `Config.Attributes.Exclude` or disabled from Span Events specifically using `Config.SpanEvents.Attributes.Exclude`. * Agent attributes added to the transaction are now also added to the root Span Event and will be visible when looking at the span in the Distributed Tracing UI. These attributes include the `request.uri` and the `request.method` along with all other attributes listed in the [attributes section of our godocs](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#pkg-constants). These agent attributes can be disabled from all destinations using `Config.Attributes.Exclude` or disabled from Span Events specifically using `Config.SpanEvents.Attributes.Exclude`. ### Bug Fixes * Fixed an issue where it was impossible to exclude the attributes `error.class` and `error.message` from the root Span Event. This issue has now been fixed. These attributes can now be excluded from all Span Events using `Config.Attributes.Exclude` or `Config.SpanEvents.Attributes.Exclude`. * Fixed an issue that caused Go's data race warnings to trigger in certain situations when using the `newrelic.NewRoundTripper`. There were no reports of actual data corruption, but now the warnings should be resolved. Thank you to @blixt for bringing this to our attention! ## 3.5.0 ### New Features * Added support for [Infinite Tracing on New Relic Edge](https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing). Infinite Tracing observes 100% of your distributed traces and provides visualizations for the most actionable data so you have the examples of errors and long-running traces so you can better diagnose and troubleshoot your systems. You [configure your agent](https://docs.newrelic.com/docs/agents/go-agent/configuration/go-agent-configuration#infinite-tracing) to send traces to a trace observer in New Relic Edge. You view your distributed traces through the New Relic’s UI. There is no need to install a collector on your network. Infinite Tracing is currently available on a sign-up basis. If you would like to participate, please contact your sales representative. **As part of this change, the Go Agent now has an added dependency on gRPC.** This is true whether or not you enable the Infinite Tracing feature. The gRPC dependencies include these two libraries: * [github.com/golang/protobuf](https://github.com/golang/protobuf) v1.3.3 * [google.golang.org/grpc](https://github.com/grpc/grpc-go) v1.27.0 You can see the changes in the [go.mod file](v3/go.mod) **As part of this change, the Go Agent now has an added dependency on gRPC.** This is true whether or not you enable the Infinite Tracing feature. The gRPC dependencies include these two libraries: * [github.com/golang/protobuf](https://github.com/golang/protobuf) v1.3.3 * [google.golang.org/grpc](https://github.com/grpc/grpc-go) v1.27.0 You can see the changes in the [go.mod file](v3/go.mod) ### Changes * [`nrgin.Middleware`](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin#Middleware) uses [`Context.FullPath()`](https://godoc.org/github.com/gin-gonic/gin#Context.FullPath) for transaction names when using Gin version 1.5.0 or greater. Gin transactions were formerly named after the [`Context.HandlerName()`](https://godoc.org/github.com/gin-gonic/gin#Context.HandlerName), which uses reflection. This change improves transaction naming and reduces overhead. Please note that because your transaction names will change, you may have to update any related dashboards and alerts to match the new name. If you wish to continue using `Context.HandlerName()` for your transaction names, use [`nrgin.MiddlewareHandlerTxnNames`](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin#MiddlewareHandlerTxnNames) instead. ```go // Transactions previously named "GET main.handleGetUsers" // will be change to something like this match the full path "GET /user/:id" ``` Note: As part of agent release v3.4.0, a v2.0.0 tag was added to the nrgin package. When using go modules however, it was impossible to install this latest version of nrgin. The v2.0.0 tag has been removed and replaced with v1.1.0. ## 3.4.0 ### New Features * Attribute `http.statusCode` has been added to external span events representing the status code on an http response. This attribute will be included when added to an ExternalSegment in one of these three ways: 1. Using [`NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewRoundTripper) with your http.Client 2. Including the http.Response as a field on your [`ExternalSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ExternalSegment) 3. Using the new [`ExternalSegment.SetStatusCode`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ExternalSegment.SetStatusCode) API to set the status code directly To exclude the `http.statusCode` attribute from span events, update your agent configuration like so, where `cfg` is your [`newrelic.Config`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Config) object. ```go cfg.SpanEvents.Attributes.Exclude = append(cfg.SpanEvents.Attributes.Exclude, newrelic.SpanAttributeHTTPStatusCode) ``` * Error attributes `error.class` and `error.message` are now included on the span event in which the error was noticed, or on the root span if an error occurs in a transaction with no segments (no chid spans). Only the most recent error information is added to the attributes; prior errors on the same span are overwritten. To exclude the `error.class` and/or `error.message` attributes from span events, update your agent configuration like so, where `cfg` is your [`newrelic.Config`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Config) object. ```go cfg.SpanEvents.Attributes.Exclude = append(cfg.SpanEvents.Attributes.Exclude, newrelic.newrelic.SpanAttributeErrorClass, newrelic.SpanAttributeErrorMessage) ``` ### Changes * Use [`Context.FullPath()`](https://godoc.org/github.com/gin-gonic/gin#Context.FullPath) for transaction names when using Gin version 1.5.0 or greater. Gin transactions were formerly named after the [`Context.HandlerName()`](https://godoc.org/github.com/gin-gonic/gin#Context.HandlerName), which uses reflection. This change improves transaction naming and reduces overhead. Please note that because your transaction names will change, you may have to update any related dashboards and alerts to match the new name. ```go // Transactions previously named "GET main.handleGetUsers" // will be change to something like this match the full path "GET /user/:id" ``` * If you are using any of these integrations, you must upgrade them when you upgrade the agent: * [nrlambda v1.1.0](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlambda) * [nrmicro v1.1.0](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmicro) * [nrnats v1.1.0](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats) * [nrstan v1.1.0](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrstan) ### Known Issues and Workarounds * If a .NET agent is initiating distributed traces as the root service, you must update that .NET agent to version 8.24 or later before upgrading your downstream Go New Relic agents to this agent release. ## 3.3.0 ### New Features * Added support for GraphQL in two new integrations: * [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go) with [v3/integrations/nrgraphgophers](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers). * [Documentation](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers) * [Example](v3/integrations/nrgraphgophers/example/main.go) * [graphql-go/graphql](https://github.com/graphql-go/graphql) with [v3/integrations/nrgraphqlgo](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo). * [Documentation](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo) * [Example](v3/integrations/nrgraphqlgo/example/main.go) * Added database instrumentation support for [snowflakedb/gosnowflake](https://github.com/snowflakedb/gosnowflake). * [Documentation](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsnowflake) * [Example](v3/integrations/nrsnowflake/example/main.go) ### Changes * When using [`newrelic.StartExternalSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#StartExternalSegment) or [`newrelic.NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewRoundTripper), if existing cross application tracing or distributed tracing headers are present on the request, they will be replaced instead of added. * The [`FromContext`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#FromContext) API which allows you to pull a Transaction from a context.Context will no longer panic if the provided context is nil. In this case, a nil is returned. ### Known Issues and Workarounds * If a .NET agent is initiating distributed traces as the root service, you must update that .NET agent to version 8.24 or later before upgrading your downstream Go New Relic agents to this agent release. ## 3.2.0 ### New Features * Added support for `v7` of [go-redis/redis](https://github.com/go-redis/redis) in the new [v3/integrations/nrredis-v7](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7) package. * [Documentation](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7) * [Example](v3/integrations/nrredis-v7/example/main.go) ### Changes * Updated Gorilla instrumentation to include request time spent in middlewares. Added new `nrgorilla.Middleware` and deprecated `nrgorilla.InstrumentRoutes`. Register the new middleware as your first middleware using [`Router.Use`](https://godoc.org/github.com/gorilla/mux#Router.Use). See the [godocs examples](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla) for more details. ```go r := mux.NewRouter() // Always register the nrgorilla.Middleware first. r.Use(nrgorilla.Middleware(app)) // All handlers and custom middlewares will be instrumented. The // transaction will be available in the Request's context. r.Use(MyCustomMiddleware) r.Handle("/", makeHandler("index")) // The NotFoundHandler and MethodNotAllowedHandler must be instrumented // separately using newrelic.WrapHandle. The second argument to // newrelic.WrapHandle is used as the transaction name; the string returned // from newrelic.WrapHandle should be ignored. _, r.NotFoundHandler = newrelic.WrapHandle(app, "NotFoundHandler", makeHandler("not found")) _, r.MethodNotAllowedHandler = newrelic.WrapHandle(app, "MethodNotAllowedHandler", makeHandler("method not allowed")) http.ListenAndServe(":8000", r) ``` ### Known Issues and Workarounds * If a .NET agent is initiating distributed traces as the root service, you must update that .NET agent to version 8.24 or later before upgrading your downstream Go New Relic agents to this agent release. ## 3.1.0 ### New Features * Support for W3C Trace Context, with easy upgrade from New Relic trace context. Distributed Tracing now supports W3C Trace Context headers for HTTP and gRPC protocols when distributed tracing is enabled. Our implementation can accept and emit both W3C trace header format and New Relic trace header format. This simplifies agent upgrades, allowing trace context to be propagated between services with older and newer releases of New Relic agents. W3C trace header format will always be accepted and emitted. New Relic trace header format will be accepted, and you can optionally disable emission of the New Relic trace header format. When distributed tracing is enabled with `Config.DistributedTracer.Enabled = true`, the Go agent will now accept W3C's `traceparent` and `tracestate` headers when calling [`Transaction.AcceptDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.AcceptDistributedTraceHeaders). When calling [`Transaction.InsertDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.InsertDistributedTraceHeaders), the Go agent will include the W3C headers along with the New Relic distributed tracing header, unless the New Relic trace header format is disabled using `Config.DistributedTracer.ExcludeNewRelicHeader = true`. * Added support for [elastic/go-elasticsearch](https://github.com/elastic/go-elasticsearch) in the new [v3/integrations/nrelasticsearch-v7](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7) package. * At this time, the New Relic backend has enabled support for real time streaming. Versions 2.8 and above will now send data to New Relic every five seconds, instead of every minute. As a result, transaction, error, and custom events will now be available in New Relic One and Insights dashboards in near real time. ### Known Issues and Workarounds * If a .NET agent is initiating distributed traces as the root service, you must update that .NET agent to version 8.24 or later before upgrading your downstream Go New Relic agents to this agent release. ## 3.0.0 We are pleased to announce the release of Go Agent v3.0.0! This is a major release that includes some breaking changes that will simplify your future use of the Go Agent. Please pay close attention to the list of Changes. ### Changes * A full list of changes and a step by step checklist on how to upgrade can be found in the [v3 Migration Guide](MIGRATION.md). ### New Features * Support for Go Modules. Our Go agent integration packages support frameworks and libraries which are changing over time. With support for Go Modules, we are now able to release instrumentation packages for multiple versions of frameworks and libraries with a single agent release; and support operation of the Go agent in Go Modules environments. This affects naming of our integration packages, as described in the v3 Migration Guide (see under "Changes" above). * Detect and set hostnames based on Heroku dyno names. When deploying an application in Heroku, the hostnames collected will now match the dyno name. This serves to greatly improve the usability of the servers list in APM since dyno names are often sporadic or fleeting in nature. The feature is controlled by two new configuration options `Config.Heroku.UseDynoNames` and `Config.Heroku.DynoNamePrefixesToShorten`. ## 2.16.3 ### New Relic's Go agent v3.0 is currently available for review and beta testing. Your use of this pre-release is at your own risk. New Relic disclaims all warranties, express or implied, regarding the beta release. ### If you do not manually take steps to use the new v3 folder you will not see any changes in your agent. This is the third release of the pre-release of Go agent v3.0. It includes changes due to user feedback during the pre-release. The existing agent in `"github.com/newrelic/go-agent"` is unchanged. The Go agent v3.0 code in the v3 folder has the following changes: * A [ConfigFromEnvironment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigFromEnvironment) bug has been fixed. ## 2.16.2 ### New Relic's Go agent v3.0 is currently available for review and beta testing. Your use of this pre-release is at your own risk. New Relic disclaims all warranties, express or implied, regarding the beta release. ### If you do not manually take steps to use the new v3 folder, as described below, you will not see any changes in your agent. This is the second release of the pre-release of Go agent v3.0. It includes changes due to user feedback during the pre-release. The existing agent in `"github.com/newrelic/go-agent"` is unchanged. The Go agent v3.0 code in the v3 folder has the following changes: * Transaction names created by [`WrapHandle`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandle), [`WrapHandleFunc`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandleFunc), [nrecho-v3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3), [nrecho-v4](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4), [nrgorilla](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla), and [nrgin](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin) now include the HTTP method. For example, the following code: ```go http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler)) ``` now creates a metric called `WebTransaction/Go/GET /users` instead of `WebTransaction/Go/users`. As a result of this change, you may need to update your alerts and dashboards. * The [ConfigFromEnvironment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigFromEnvironment) config option is now strict. If one of the environment variables, such as `NEW_RELIC_DISTRIBUTED_TRACING_ENABLED`, cannot be parsed, then `Config.Error` will be populated and [NewApplication](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewApplication) will return an error. * [ConfigFromEnvironment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigFromEnvironment) now processes `NEW_RELIC_ATTRIBUTES_EXCLUDE` and `NEW_RELIC_ATTRIBUTES_INCLUDE`. ## 2.16.1 ### New Relic's Go agent v3.0 is currently available for review and beta testing. Your use of this pre-release is at your own risk. New Relic disclaims all warranties, express or implied, regarding the beta release. ### If you do not manually take steps to use the new v3 folder, as described below, you will not see any changes in your agent. This 2.16.1 release includes a new v3.0 folder which contains the pre-release of Go agent v3.0; Go agent v3.0 includes breaking changes. We are seeking feedback and hope that you will look this over and test out the changes prior to the official release. **This is not an official 3.0 release, it is just a vehicle to gather feedback on proposed changes**. It is not tagged as 3.0 in Github and the 3.0 release is not yet available to update in your Go mod file. In order to test out these changes, you will need to clone this repo in your Go source directory, under `[go-src-dir]/src/github.com/newrelic/go-agent`. Once you have the source checked out, you will need to follow the steps in the second section of [v3/MIGRATION.md](v3/MIGRATION.md). A list of changes and installation instructions is included in the v3 folder and can be found [here](v3/MIGRATION.md) For this pre-release (beta) version of Go agent v3.0, please note: * The changes in the v3 folder represent what we expect to release in ~2 weeks as our major 3.0 release. However, as we are soliciting feedback on the changes and there is the possibility of some breaking changes before the official release. * This is not an official 3.0 release; it is not tagged as 3.0 in Github and the 3.0 release is not yet available to update in your Go mod file. * If you test out these changes and encounter issues, questions, or have feedback that you would like to pass along, please open up an issue [here](https://github.com/newrelic/go-agent/issues/new) and be sure to include the label `3.0`. * For normal (non-3.0) issues/questions we request that you report them via our [support site](https://support.newrelic.com/) or our [community forum](https://discuss.newrelic.com). Please only report questions related to the 3.0 pre-release directly via GitHub. ### New Features * V3 will add support for Go Modules. The go.mod files exist in the v3 folder, but they will not be usable until we have fully tagged the 3.0 release officially. Examples of version tags we plan to use for different modules include: * `v3.0.0` * `v3/integrations/nrecho-v3/v1.0.0` * `v3/integrations/nrecho-v4/v1.0.0` ### Changes * The changes are the ones that we have requested feedback previously in [this issue](https://github.com/newrelic/go-agent/issues/106). * A full list of changes that are included, along with a checklist for upgrading, is available in [v3/MIGRATION.md](v3/MIGRATION.md). ## 2.16.0 ### Upcoming * The next release of the Go Agent is expected to be a major version release to improve the API and incorporate Go modules. Details available here: https://github.com/newrelic/go-agent/issues/106 We would love your feedback! ### Bug Fixes * Fixed an issue in the [`nrhttprouter`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) integration where the transaction was not being added to the requests context. This resulted in an inability to access the transaction from within an [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Handle) function. This issue has now been fixed. ## 2.15.0 ### New Features * Added support for monitoring [MongoDB](https://github.com/mongodb/mongo-go-driver/) queries with the new [_integrations/nrmongo](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmongo) package. * [Example application](https://github.com/newrelic/go-agent/blob/master/_integrations/nrmongo/example/main.go) * [Full godocs Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmongo) * Added new method `Transaction.IsSampled()` that returns a boolean that indicates if the transaction is sampled. A sampled transaction records a span event for each segment. Distributed tracing must be enabled for transactions to be sampled. `false` is returned if the transaction has finished. This sampling flag is needed for B3 trace propagation and future support of W3C Trace Context. * Added support for adding [B3 Headers](https://github.com/openzipkin/b3-propagation) to outgoing requests. This is helpful if the service you are calling uses B3 for trace state propagation (for example, it uses Zipkin instrumentation). You can use the new [_integrations/nrb3](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrb3) package's [`nrb3.NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrb3#NewRoundTripper) like this: ```go // When defining the client, set the Transport to the NewRoundTripper. This // will create ExternalSegments and add B3 headers for each request. client := &http.Client{ Transport: nrb3.NewRoundTripper(nil), } // Distributed Tracing must be enabled for this application. // (see https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing) txn := currentTxn() req, err := http.NewRequest("GET", "https://example.com", nil) if nil != err { log.Fatalln(err) } // Be sure to add the transaction to the request context. This step is // required. req = newrelic.RequestWithTransactionContext(req, txn) resp, err := client.Do(req) if nil != err { log.Fatalln(err) } defer resp.Body.Close() fmt.Println(resp.StatusCode) ``` ### Bug Fixes * Fixed an issue where the [`nrgin`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1) integration was not capturing the correct response code in the case where no response body was sent. This issue has now been fixed but requires Gin greater than v1.4.0. ## 2.14.1 ### Bug Fixes * Removed the hidden `"NEW_RELIC_DEBUG_LOGGING"` environment variable setting which was broken in release 2.14.0. ## 2.14.0 ### New Features * Added support for a new segment type, [`MessageProducerSegment`](https://godoc.org/github.com/newrelic/go-agent#MessageProducerSegment), to be used to track time spent adding messages to message queuing systems like RabbitMQ or Kafka. ```go seg := &newrelic.MessageProducerSegment{ StartTime: newrelic.StartSegmentNow(txn), Library: "RabbitMQ", DestinationType: newrelic.MessageExchange, DestinationName: "myExchange", } // add message to queue here seg.End() ``` * Added new attribute constants for use with message consumer transactions. These attributes can be used to add more detail to a transaction that tracks time spent consuming a message off a message queuing system like RabbitMQ or Kafka. They can be added using [`txn.AddAttribute`](https://godoc.org/github.com/newrelic/go-agent#Transaction). ```go // The routing key of the consumed message. txn.AddAttribute(newrelic.AttributeMessageRoutingKey, "myRoutingKey") // The name of the queue the message was consumed from. txn.AddAttribute(newrelic.AttributeMessageQueueName, "myQueueName") // The type of exchange used for the consumed message (direct, fanout, // topic, or headers). txn.AddAttribute(newrelic.AttributeMessageExchangeType, "myExchangeType") // The callback queue used in RPC configurations. txn.AddAttribute(newrelic.AttributeMessageReplyTo, "myReplyTo") // The application-generated identifier used in RPC configurations. txn.AddAttribute(newrelic.AttributeMessageCorrelationID, "myCorrelationID") ``` It is recommended that at most one message is consumed per transaction. * Added support for [Go 1.13's Error wrapping](https://golang.org/doc/go1.13#error_wrapping). `Transaction.NoticeError` now uses [Unwrap](https://golang.org/pkg/errors/#Unwrap) recursively to identify the error's cause (the deepest wrapped error) when generating the error's class field. This functionality will help group your errors usefully. For example, when using Go 1.13, the following code: ```go type socketError struct{} func (e socketError) Error() string { return "socket error" } func gamma() error { return socketError{} } func beta() error { return fmt.Errorf("problem in beta: %w", gamma()) } func alpha() error { return fmt.Errorf("problem in alpha: %w", beta()) } func execute(txn newrelic.Transaction) { err := alpha() txn.NoticeError(err) } ``` captures an error with message `"problem in alpha: problem in beta: socket error"` and class `"main.socketError"`. Previously, the class was recorded as `"*fmt.wrapError"`. * A `Stack` field has been added to [Error](https://godoc.org/github.com/newrelic/go-agent#Error), which can be assigned using the new [NewStackTrace](https://godoc.org/github.com/newrelic/go-agent#NewStackTrace) function. This allows your error stack trace to show where the error happened, rather than the location of the `NoticeError` call. `Transaction.NoticeError` not only checks for a stack trace (using [StackTracer](https://godoc.org/github.com/newrelic/go-agent#StackTracer)) in the error parameter, but in the error's cause as well. This means that you can create an [Error](https://godoc.org/github.com/newrelic/go-agent#Error) where your error occurred, wrap it multiple times to add information, notice it with `NoticeError`, and still have a useful stack trace. Take a look! ```go func gamma() error { return newrelic.Error{ Message: "something went very wrong", Class: "socketError", Stack: newrelic.NewStackTrace(), } } func beta() error { return fmt.Errorf("problem in beta: %w", gamma()) } func alpha() error { return fmt.Errorf("problem in alpha: %w", beta()) } func execute(txn newrelic.Transaction) { err := alpha() txn.NoticeError(err) } ``` In this example, the topmost stack trace frame recorded is `"gamma"`, rather than `"execute"`. * Added support for configuring a maximum number of transaction events per minute to be sent to New Relic. It can be configured as follows: ```go config := newrelic.NewConfig("Application Name", os.Getenv("NEW_RELIC_LICENSE_KEY")) config.TransactionEvents.MaxSamplesStored = 100 ``` * For additional configuration information, see our [documentation](https://docs.newrelic.com/docs/agents/go-agent/configuration/go-agent-configuration) ### Miscellaneous * Updated the [`nrmicro`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro) package to use the new segment type [`MessageProducerSegment`](https://godoc.org/github.com/newrelic/go-agent#MessageProducerSegment) and the new attribute constants: * [`nrmicro.ClientWrapper`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro#ClientWrapper) now uses `newrelic.MessageProducerSegment`s instead of `newrelic.ExternalSegment`s for calls to [`Client.Publish`](https://godoc.org/github.com/micro/go-micro/client#Client). * [`nrmicro.SubscriberWrapper`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro#SubscriberWrapper) updates transaction names and adds the attribute `message.routingKey`. * Updated the [`nrnats`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats) and [`nrstan`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan) packages to use the new segment type [`MessageProducerSegment`](https://godoc.org/github.com/newrelic/go-agent#MessageProducerSegment) and the new attribute constants: * [`nrnats.StartPublishSegment`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats#StartPublishSegment) now starts and returns a `newrelic.MessageProducerSegment` type. * [`nrnats.SubWrapper`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats#SubWrapper) and [`nrstan.StreamingSubWrapper`](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan#StreamingSubWrapper) updates transaction names and adds the attributes `message.routingKey`, `message.queueName`, and `message.replyTo`. ## 2.13.0 ### New Features * Added support for [HttpRouter](https://github.com/julienschmidt/httprouter) in the new [_integrations/nrhttprouter](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) package. This package allows you to easily instrument inbound requests through the HttpRouter framework. * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) * [Example](_integrations/nrhttprouter/example/main.go) * Added support for [github.com/uber-go/zap](https://github.com/uber-go/zap) in the new [_integrations/nrzap](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrzap) package. This package allows you to send agent log messages to `zap`. ## 2.12.0 ### New Features * Added new methods to expose `Transaction` details: * `Transaction.GetTraceMetadata()` returns a [TraceMetadata](https://godoc.org/github.com/newrelic/go-agent#TraceMetadata) which contains distributed tracing identifiers. * `Transaction.GetLinkingMetadata()` returns a [LinkingMetadata](https://godoc.org/github.com/newrelic/go-agent#LinkingMetadata) which contains the fields needed to link data to a trace or entity. * Added a new plugin for the [Logrus logging framework](https://github.com/sirupsen/logrus) with the new [_integrations/logcontext/nrlogrusplugin](https://github.com/newrelic/go-agent/go-agent/tree/master/_integrations/logcontext/nrlogrusplugin) package. This plugin leverages the new `GetTraceMetadata` and `GetLinkingMetadata` above to decorate logs. To enable, set your log's formatter to the `nrlogrusplugin.ContextFormatter{}` ```go logger := logrus.New() logger.SetFormatter(nrlogrusplugin.ContextFormatter{}) ``` The logger will now look for a `newrelic.Transaction` inside its context and decorate logs accordingly. Therefore, the Transaction must be added to the context and passed to the logger. For example, this logging call ```go logger.Info("Hello New Relic!") ``` must be transformed to include the context, such as: ```go ctx := newrelic.NewContext(context.Background(), txn) logger.WithContext(ctx).Info("Hello New Relic!") ``` For full documentation see the [godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin) or view the [example](https://github.com/newrelic/go-agent/blob/master/_integrations/logcontext/nrlogrusplugin/example/main.go). * Added support for [NATS](https://github.com/nats-io/nats.go) and [NATS Streaming](https://github.com/nats-io/stan.go) monitoring with the new [_integrations/nrnats](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats) and [_integrations/nrstan](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan) packages. These packages support instrumentation of publishers and subscribers. * [NATS Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrnats/examples/main.go) * [NATS Streaming Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrstan/examples/main.go) * Enables ability to migrate to [Configurable Security Policies (CSP)](https://docs.newrelic.com/docs/agents/manage-apm-agents/configuration/enable-configurable-security-policies) on a per agent basis for accounts already using [High Security Mode (HSM)](https://docs.newrelic.com/docs/agents/manage-apm-agents/configuration/high-security-mode). * Previously, if CSP was configured for an account, New Relic would not allow an agent to connect without the `security_policies_token`. This led to agents not being able to connect during the period between when CSP was enabled for an account and when each agent is configured with the correct token. * With this change, when both HSM and CSP are enabled for an account, an agent (this version or later) can successfully connect with either `high_security: true` or the appropriate `security_policies_token` configured - allowing the agent to continue to connect after CSP is configured on the account but before the appropriate `security_policies_token` is configured for each agent. ## 2.11.0 ### New Features * Added support for [Micro](https://github.com/micro/go-micro) monitoring with the new [_integrations/nrmicro](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro) package. This package supports instrumentation for servers, clients, publishers, and subscribers. * [Server Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrmicro/example/server/server.go) * [Client Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrmicro/example/client/client.go) * [Publisher and Subscriber Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrmicro/example/pubsub/main.go) * [Full godocs Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro) * Added support for creating static `WebRequest` instances manually via the `NewStaticWebRequest` function. This can be useful when you want to create a web transaction but don't have an `http.Request` object. Here's an example of creating a static `WebRequest` and using it to mark a transaction as a web transaction: ```go hdrs := http.Headers{} u, _ := url.Parse("https://example.com") webReq := newrelic.NewStaticWebRequest(hdrs, u, "GET", newrelic.TransportHTTP) txn := app.StartTransaction("My-Transaction", nil, nil) txn.SetWebRequest(webReq) ``` ## 2.10.0 ### New Features * Added support for custom events when using [nrlambda](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlambda). Example Lambda handler which creates custom event: ```go func handler(ctx context.Context) { if txn := newrelic.FromContext(ctx); nil != txn { txn.Application().RecordCustomEvent("myEvent", map[string]interface{}{ "zip": "zap", }) } fmt.Println("hello world!") } ``` ## 2.9.0 ### New Features * Added support for [gRPC](https://github.com/grpc/grpc-go) monitoring with the new [_integrations/nrgrpc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgrpc) package. This package supports instrumentation for servers and clients. * [Server Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/server/server.go) * [Client Example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/client/client.go) * Added new [ExternalSegment](https://godoc.org/github.com/newrelic/go-agent#ExternalSegment) fields `Host`, `Procedure`, and `Library`. These optional fields are automatically populated from the segment's `URL` or `Request` if unset. Use them if you don't have access to a request or URL but still want useful external metrics, transaction segment attributes, and span attributes. * `Host` is used for external metrics, transaction trace segment names, and span event names. The host of segment's `Request` or `URL` is the default. * `Procedure` is used for transaction breakdown metrics. If set, it should be set to the remote procedure being called. The HTTP method of the segment's `Request` is the default. * `Library` is used for external metrics and the `"component"` span attribute. If set, it should be set to the framework making the call. `"http"` is the default. With the addition of these new fields, external transaction breakdown metrics are changed: `External/myhost.com/all` will now report as `External/myhost.com/http/GET` (provided the HTTP method is `GET`). * HTTP Response codes below `100`, except `0` and `5`, are now recorded as errors. This is to support `gRPC` status codes. If you start seeing new status code errors that you would like to ignore, add them to `Config.ErrorCollector.IgnoreStatusCodes` or your server side configuration settings. * Improve [logrus](https://github.com/sirupsen/logrus) support by introducing [nrlogrus.Transform](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus#Transform), a function which allows you to turn a [logrus.Logger](https://godoc.org/github.com/sirupsen/logrus#Logger) instance into a [newrelic.Logger](https://godoc.org/github.com/newrelic/go-agent#Logger). Example use: ```go l := logrus.New() l.SetLevel(logrus.DebugLevel) cfg := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__") cfg.Logger = nrlogrus.Transform(l) ``` As a result of this change, the [nrlogrus](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus) package requires [logrus](https://github.com/sirupsen/logrus) version `v1.1.0` and above. ## 2.8.1 ### Bug Fixes * Removed `nrmysql.NewConnector` since [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) has not yet released `mysql.NewConnector`. ## 2.8.0 ### New Features * Support for Real Time Streaming * The agent now has support for sending event data to New Relic every five seconds, instead of every minute. As a result, transaction, error, and custom events will now be available in New Relic One and Insights dashboards in near real time. For more information on how to view your events with a five-second refresh, see the documentation. * Note that the overall limits on how many events can be sent per minute have not changed. Also, span events, metrics, and trace data is unaffected, and will still be sent every minute. * Introduce support for databases using [database/sql](https://golang.org/pkg/database/sql/). This new functionality allows you to instrument MySQL, PostgreSQL, and SQLite calls without manually creating [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment)s. | Database Library Supported | Integration Package | | ------------- | ------------- | | [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) | [_integrations/nrmysql](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmysql) | | [lib/pq](https://github.com/lib/pq) | [_integrations/nrpq](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpq) | | [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) | [_integrations/nrsqlite3](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrsqlite3) | Using these database integration packages is easy! First replace the driver with our integration version: ```go import ( // import our integration package in place of "github.com/go-sql-driver/mysql" _ "github.com/newrelic/go-agent/_integrations/nrmysql" ) func main() { // open "nrmysql" in place of "mysql" db, err := sql.Open("nrmysql", "user@unix(/path/to/socket)/dbname") } ``` Second, use the `ExecContext`, `QueryContext`, and `QueryRowContext` methods of [sql.DB](https://golang.org/pkg/database/sql/#DB), [sql.Conn](https://golang.org/pkg/database/sql/#Conn), [sql.Tx](https://golang.org/pkg/database/sql/#Tx), and [sql.Stmt](https://golang.org/pkg/database/sql/#Stmt) and provide a transaction-containing context. Calls to `Exec`, `Query`, and `QueryRow` do not get instrumented. ```go ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) from tables") ``` If you are using a [database/sql](https://golang.org/pkg/database/sql/) database not listed above, you can write your own instrumentation for it using [InstrumentSQLConnector](https://godoc.org/github.com/newrelic/go-agent#InstrumentSQLConnector), [InstrumentSQLDriver](https://godoc.org/github.com/newrelic/go-agent#InstrumentSQLDriver), and [SQLDriverSegmentBuilder](https://godoc.org/github.com/newrelic/go-agent#SQLDriverSegmentBuilder). The integration packages act as examples of how to do this. For more information, see the [Go agent documentation on instrumenting datastore segments](https://docs.newrelic.com/docs/agents/go-agent/instrumentation/instrument-go-segments#go-datastore-segments). ### Bug Fixes * The [http.RoundTripper](https://golang.org/pkg/net/http/#RoundTripper) returned by [NewRoundTripper](https://godoc.org/github.com/newrelic/go-agent#NewRoundTripper) no longer modifies the request. Our thanks to @jlordiales for the contribution. ## 2.7.0 ### New Features * Added support for server side configuration. Server side configuration allows you to set the following configuration settings in the New Relic APM UI: * `Config.TransactionTracer.Enabled` * `Config.ErrorCollector.Enabled` * `Config.CrossApplicationTracer.Enabled` * `Config.TransactionTracer.Threshold` * `Config.TransactionTracer.StackTraceThreshold` * `Config.ErrorCollector.IgnoreStatusCodes` For more information see the [server side configuration documentation](https://docs.newrelic.com/docs/agents/manage-apm-agents/configuration/server-side-agent-configuration). * Added support for AWS Lambda functions in the new [nrlambda](_integrations/nrlambda) package. Please email if you are interested in learning more or previewing New Relic Lambda monitoring. This instrumentation package requires `aws-lambda-go` version [v1.9.0](https://github.com/aws/aws-lambda-go/releases) and above. * [documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlambda) * [working example](_integrations/nrlambda/example/main.go) ## 2.6.0 ### New Features * Added support for async: the ability to instrument multiple concurrent goroutines, or goroutines that access or manipulate the same Transaction. The new `Transaction.NewGoroutine() Transaction` method allows transactions to create segments in multiple goroutines! `NewGoroutine` returns a new reference to the `Transaction`. This must be called any time you are passing the `Transaction` to another goroutine which makes segments. Each segment-creating goroutine must have its own `Transaction` reference. It does not matter if you call this before or after the other goroutine has started. All `Transaction` methods can be used in any `Transaction` reference. The `Transaction` will end when `End()` is called in any goroutine. Example passing a new `Transaction` reference directly to another goroutine: ```go go func(txn newrelic.Transaction) { defer newrelic.StartSegment(txn, "async").End() time.Sleep(100 * time.Millisecond) }(txn.NewGoroutine()) ``` Example passing a new `Transaction` reference on a channel to another goroutine: ```go ch := make(chan newrelic.Transaction) go func() { txn := <-ch defer newrelic.StartSegment(txn, "async").End() time.Sleep(100 * time.Millisecond) }() ch <- txn.NewGoroutine() ``` * Added integration support for [`aws-sdk-go`](https://github.com/aws/aws-sdk-go) and [`aws-sdk-go-v2`](https://github.com/aws/aws-sdk-go-v2). When using these SDKs, a segment will be created for each out going request. For DynamoDB calls, these will be Datastore segments and for all others they will be External segments. * [v1 Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v1) * [v2 Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v2) * Added span event and transaction trace segment attribute configuration. You may control which attributes are captured in span events and transaction trace segments using the `Config.SpanEvents.Attributes` and `Config.TransactionTracer.Segments.Attributes` settings. For example, if you want to disable the collection of `"db.statement"` in your span events, modify your config like this: ```go cfg.SpanEvents.Attributes.Exclude = append(cfg.SpanEvents.Attributes.Exclude, newrelic.SpanAttributeDBStatement) ``` To disable the collection of all attributes from your transaction trace segments, modify your config like this: ```go cfg.TransactionTracer.Segments.Attributes.Enabled = false ``` ### Bug Fixes * Fixed a bug that would prevent External Segments from being created under certain error conditions related to Cross Application Tracing. ### Miscellaneous * Improved linking between Cross Application Transaction Traces in the APM UI. When `Config.CrossApplicationTracer.Enabled = true`, External segments in the Transaction Traces details will now link to the downstream Transaction Trace if there is one. Additionally, the segment name will now include the name of the downstream application and the name of the downstream transaction. * Update attribute names of Datastore and External segments on Transaction Traces to be in line with attribute names on Spans. Specifically: * `"uri"` => `"http.url"` * `"query"` => `"db.statement"` * `"database_name"` => `"db.instance"` * `"host"` => `"peer.hostname"` * `"port_path_or_id"` + `"host"` => `"peer.address"` ## 2.5.0 * Added support for [New Relic Browser](https://docs.newrelic.com/docs/browser) using the new `BrowserTimingHeader` method on the [`Transaction`](https://godoc.org/github.com/newrelic/go-agent#Transaction) which returns a [BrowserTimingHeader](https://godoc.org/github.com/newrelic/go-agent#BrowserTimingHeader). The New Relic Browser JavaScript code measures page load timing, also known as real user monitoring. The Pro version of this feature measures AJAX requests, single-page applications, JavaScript errors, and much more! Example use: ```go func browser(w http.ResponseWriter, r *http.Request) { hdr, err := w.(newrelic.Transaction).BrowserTimingHeader() if nil != err { log.Printf("unable to create browser timing header: %v", err) } // BrowserTimingHeader() will always return a header whose methods can // be safely called. if js := hdr.WithTags(); js != nil { w.Write(js) } io.WriteString(w, "browser header page") } ``` * The Go agent now collects an attribute named `request.uri` on Transaction Traces, Transaction Events, Error Traces, and Error Events. `request.uri` will never contain user, password, query parameters, or fragment. To prevent the request's URL from being collected in any data, modify your `Config` like this: ```go cfg.Attributes.Exclude = append(cfg.Attributes.Exclude, newrelic.AttributeRequestURI) ``` ## 2.4.0 * Introduced `Transaction.Application` method which returns the `Application` that started the `Transaction`. This method is useful since it may prevent having to pass the `Application` to code that already has access to the `Transaction`. Example use: ```go txn.Application().RecordCustomEvent("customerOrder", map[string]interface{}{ "numItems": 2, "totalPrice": 13.75, }) ``` * The `Transaction.AddAttribute` method no longer accepts `nil` values since our backend ignores them. ## 2.3.0 * Added support for [Echo](https://echo.labstack.com) in the new `nrecho` package. * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrecho) * [Example](_integrations/nrecho/example/main.go) * Introduced `Transaction.SetWebResponse(http.ResponseWriter)` method which sets the transaction's response writer. After calling this method, the `Transaction` may be used in place of the `http.ResponseWriter` to intercept the response code. This method is useful when the `http.ResponseWriter` is not available at the beginning of the transaction (if so, it can be given as a parameter to `Application.StartTransaction`). This method will return a reference to the transaction which implements the combination of `http.CloseNotifier`, `http.Flusher`, `http.Hijacker`, and `io.ReaderFrom` implemented by the ResponseWriter. Example: ```go func setResponseDemo(txn newrelic.Transaction) { recorder := httptest.NewRecorder() txn = txn.SetWebResponse(recorder) txn.WriteHeader(200) fmt.Println("response code recorded:", recorder.Code) } ``` * The `Transaction`'s `http.ResponseWriter` methods may now be called safely if a `http.ResponseWriter` has not been set. This allows you to add a response code to the transaction without using a `http.ResponseWriter`. Example: ```go func transactionWithResponseCode(app newrelic.Application) { txn := app.StartTransaction("hasResponseCode", nil, nil) defer txn.End() txn.WriteHeader(200) // Safe! } ``` * The agent now collects environment variables prefixed by `NEW_RELIC_METADATA_`. Some of these may be added Transaction events to provide context between your Kubernetes cluster and your services. For details on the benefits (currently in beta) see [this blog post](https://blog.newrelic.com/engineering/monitoring-application-performance-in-kubernetes/) * The agent now collects the `KUBERNETES_SERVICE_HOST` environment variable to detect when the application is running on Kubernetes. * The agent now collects the fully qualified domain name of the host and local IP addresses for improved linking with our infrastructure product. ## 2.2.0 * The `Transaction` parameter to [NewRoundTripper](https://godoc.org/github.com/newrelic/go-agent#NewRoundTripper) and [StartExternalSegment](https://godoc.org/github.com/newrelic/go-agent#StartExternalSegment) is now optional: If it is `nil`, then a `Transaction` will be looked for in the request's context (using [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext)). Passing a `nil` transaction is **STRONGLY** recommended when using [NewRoundTripper](https://godoc.org/github.com/newrelic/go-agent#NewRoundTripper) since it allows one `http.Client.Transport` to be used for multiple transactions. Example use: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(nil, client.Transport) request, _ := http.NewRequest("GET", "https://example.com", nil) request = newrelic.RequestWithTransactionContext(request, txn) resp, err := client.Do(request) ``` * Introduced `Transaction.SetWebRequest(WebRequest)` method which marks the transaction as a web transaction. If the `WebRequest` parameter is non-nil, `SetWebRequest` will collect details on request attributes, url, and method. This method is useful if you don't have access to the request at the beginning of the transaction, or if your request is not an `*http.Request` (just add methods to your request that satisfy [WebRequest](https://godoc.org/github.com/newrelic/go-agent#WebRequest)). To use an `*http.Request` as the parameter, use the [NewWebRequest](https://godoc.org/github.com/newrelic/go-agent#NewWebRequest) transformation function. Example: ```go var request *http.Request = getInboundRequest() txn.SetWebRequest(newrelic.NewWebRequest(request)) ``` * Fixed `Debug` in `nrlogrus` package. Previous versions of the New Relic Go Agent incorrectly logged to Info level instead of Debug. This has now been fixed. Thanks to @paddycarey for catching this. * [nrgin.Transaction](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1#Transaction) may now be called with either a `context.Context` or a `*gin.Context`. If you were passing a `*gin.Context` around your functions as a `context.Context`, you may access the Transaction by calling either [nrgin.Transaction](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1#Transaction) or [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext). These functions now work nicely together. For example, [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext) will return the `Transaction` added by [nrgin.Middleware](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1#Middleware). Thanks to @rodriguezgustavo for the suggestion. ## 2.1.0 * The Go Agent now supports distributed tracing. Distributed tracing lets you see the path that a request takes as it travels through your distributed system. By showing the distributed activity through a unified view, you can troubleshoot and understand a complex system better than ever before. Distributed tracing is available with an APM Pro or equivalent subscription. To see a complete distributed trace, you need to enable the feature on a set of neighboring services. Enabling distributed tracing changes the behavior of some New Relic features, so carefully consult the [transition guide](https://docs.newrelic.com/docs/transition-guide-distributed-tracing) before you enable this feature. To enable distributed tracing, set the following fields in your config. Note that distributed tracing and cross application tracing cannot be used simultaneously. ``` config := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__") config.CrossApplicationTracer.Enabled = false config.DistributedTracer.Enabled = true ``` Please refer to the [distributed tracing section of the guide](GUIDE.md#distributed-tracing) for more detail on how to ensure you get the most out of the Go agent's distributed tracing support. * Added functions [NewContext](https://godoc.org/github.com/newrelic/go-agent#NewContext) and [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext) for adding and retrieving the Transaction from a Context. Handlers instrumented by [WrapHandle](https://godoc.org/github.com/newrelic/go-agent#WrapHandle), [WrapHandleFunc](https://godoc.org/github.com/newrelic/go-agent#WrapHandleFunc), and [nrgorilla.InstrumentRoutes](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1#InstrumentRoutes) may use [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext) on the request's context to access the Transaction. Thanks to @caarlos0 for the contribution! Though [NewContext](https://godoc.org/github.com/newrelic/go-agent#NewContext) and [FromContext](https://godoc.org/github.com/newrelic/go-agent#FromContext) require Go 1.7+ (when [context](https://golang.org/pkg/context/) was added), [RequestWithTransactionContext](https://godoc.org/github.com/newrelic/go-agent#RequestWithTransactionContext) is always exported so that it can be used in all framework and library instrumentation. ## 2.0.0 * The `End()` functions defined on the `Segment`, `DatastoreSegment`, and `ExternalSegment` types now receive the segment as a pointer, rather than as a value. This prevents unexpected behaviour when a call to `End()` is deferred before one or more fields are changed on the segment. In practice, this is likely to only affect this pattern: ```go defer newrelic.DatastoreSegment{ // ... }.End() ``` Instead, you will now need to separate the literal from the deferred call: ```go ds := newrelic.DatastoreSegment{ // ... } defer ds.End() ``` When creating custom and external segments, we recommend using [`newrelic.StartSegment()`](https://godoc.org/github.com/newrelic/go-agent#StartSegment) and [`newrelic.StartExternalSegment()`](https://godoc.org/github.com/newrelic/go-agent#StartExternalSegment), respectively. * Added GoDoc badge to README. Thanks to @mrhwick for the contribution! * `Config.UseTLS` configuration setting has been removed to increase security. TLS will now always be used in communication with New Relic Servers. ## 1.11.0 * We've closed the Issues tab on GitHub. Please visit our [support site](https://support.newrelic.com) to get timely help with any problems you're having, or to report issues. * Added support for Cross Application Tracing (CAT). Please refer to the [CAT section of the guide](GUIDE.md#cross-application-tracing) for more detail on how to ensure you get the most out of the Go agent's new CAT support. * The agent now collects additional metadata when running within Amazon Web Services, Google Cloud Platform, Microsoft Azure, and Pivotal Cloud Foundry. This information is used to provide an enhanced experience when the agent is deployed on those platforms. ## 1.10.0 * Added new `RecordCustomMetric` method to [Application](https://godoc.org/github.com/newrelic/go-agent#Application). This functionality can be used to track averages or counters without using custom events. * [Custom Metric Documentation](https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/collect-custom-metrics) * Fixed import needed for logrus. The import Sirupsen/logrus had been renamed to sirupsen/logrus. Thanks to @alfred-landrum for spotting this. * Added [ErrorAttributer](https://godoc.org/github.com/newrelic/go-agent#ErrorAttributer), an optional interface that can be implemented by errors provided to `Transaction.NoticeError` to attach additional attributes. These attributes are subject to attribute configuration. * Added [Error](https://godoc.org/github.com/newrelic/go-agent#Error), a type that allows direct control of error fields. Example use: ```go txn.NoticeError(newrelic.Error{ // Message is returned by the Error() method. Message: "error message: something went very wrong", Class: "errors are aggregated by class", Attributes: map[string]interface{}{ "important_number": 97232, "relevant_string": "zap", }, }) ``` * Updated license to address scope of usage. ## 1.9.0 * Added support for [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) in the new `nrgin` package. * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1) * [Example](examples/_gin/main.go) ## 1.8.0 * Fixed incorrect metric rule application when the metric rule is flagged to terminate and matches but the name is unchanged. * `Segment.End()`, `DatastoreSegment.End()`, and `ExternalSegment.End()` methods now return an error which may be helpful in diagnosing situations where segment data is unexpectedly missing. ## 1.7.0 * Added support for [gorilla/mux](https://github.com/gorilla/mux) in the new `nrgorilla` package. * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1) * [Example](examples/_gorilla/main.go) ## 1.6.0 * Added support for custom error messages and stack traces. Errors provided to `Transaction.NoticeError` will now be checked to see if they implement [ErrorClasser](https://godoc.org/github.com/newrelic/go-agent#ErrorClasser) and/or [StackTracer](https://godoc.org/github.com/newrelic/go-agent#StackTracer). Thanks to @fgrosse for this proposal. * Added support for [pkg/errors](https://github.com/pkg/errors). Thanks to @fgrosse for this work. * [documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpkgerrors) * [example](https://github.com/newrelic/go-agent/blob/master/_integrations/nrpkgerrors/nrpkgerrors.go) * Fixed tests for Go 1.8. ## 1.5.0 * Added support for Windows. Thanks to @ianomad and @lvxv for the contributions. * The number of heap objects allocated is recorded in the `Memory/Heap/AllocatedObjects` metric. This will soon be displayed on the "Go runtime" page. * If the [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment) fields `Host` and `PortPathOrID` are not provided, they will no longer appear as `"unknown"` in transaction traces and slow query traces. * Stack traces will now be nicely aligned in the APM UI. ## 1.4.0 * Added support for slow query traces. Slow datastore segments will now generate slow query traces viewable on the datastore tab. These traces include a stack trace and help you to debug slow datastore activity. [Slow Query Documentation](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/viewing-slow-query-details) * Added new [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment) fields `ParameterizedQuery`, `QueryParameters`, `Host`, `PortPathOrID`, and `DatabaseName`. These fields will be shown in transaction traces and in slow query traces. ## 1.3.0 * Breaking Change: Added a timeout parameter to the `Application.Shutdown` method. ## 1.2.0 * Added support for instrumenting short-lived processes: * The new `Application.Shutdown` method allows applications to report data to New Relic without waiting a full minute. * The new `Application.WaitForConnection` method allows your process to defer instrumentation until the application is connected and ready to gather data. * Full documentation here: [application.go](application.go) * Example short-lived process: [examples/short-lived-process/main.go](examples/short-lived-process/main.go) * Error metrics are no longer created when `ErrorCollector.Enabled = false`. * Added support for [github.com/mgutz/logxi](github.com/mgutz/logxi). See [_integrations/nrlogxi/v1/nrlogxi.go](_integrations/nrlogxi/v1/nrlogxi.go). * Fixed bug where Transaction Trace thresholds based upon Apdex were not being applied to background transactions. ## 1.1.0 * Added support for Transaction Traces. * Stack trace filenames have been shortened: Any thing preceding the first `/src/` is now removed. ## 1.0.0 * Removed `BetaToken` from the `Config` structure. * Breaking Datastore Change: `datastore` package contents moved to top level `newrelic` package. `datastore.MySQL` has become `newrelic.DatastoreMySQL`. * Breaking Attributes Change: `attributes` package contents moved to top level `newrelic` package. `attributes.ResponseCode` has become `newrelic.AttributeResponseCode`. Some attribute name constants have been shortened. * Added "runtime.NumCPU" to the environment tab. Thanks sergeylanzman for the contribution. * Prefixed the environment tab values "Compiler", "GOARCH", "GOOS", and "Version" with "runtime.". ## 0.8.0 * Breaking Segments API Changes: The segments API has been rewritten with the goal of being easier to use and to avoid nil Transaction checks. See: * [segments.go](segments.go) * [examples/server/main.go](examples/server/main.go) * [GUIDE.md#segments](GUIDE.md#segments) * Updated LICENSE.txt with contribution information. ## 0.7.1 * Fixed a bug causing the `Config` to fail to serialize into JSON when the `Transport` field was populated. ## 0.7.0 * Eliminated `api`, `version`, and `log` packages. `Version`, `Config`, `Application`, and `Transaction` now live in the top level `newrelic` package. If you imported the `attributes` or `datastore` packages then you will need to remove `api` from the import path. * Breaking Logging Changes Logging is no longer controlled though a single global. Instead, logging is configured on a per-application basis with the new `Config.Logger` field. The logger is an interface described in [log.go](log.go). See [GUIDE.md#logging](GUIDE.md#logging). ## 0.6.1 * No longer create "GC/System/Pauses" metric if no GC pauses happened. ## 0.6.0 * Introduced beta token to support our beta program. * Rename `Config.Development` to `Config.Enabled` (and change boolean direction). * Fixed a bug where exclusive time could be incorrect if segments were not ended. * Fix unit tests broken in 1.6. * In `Config.Enabled = false` mode, the license must be the proper length or empty. * Added runtime statistics for CPU/memory usage, garbage collection, and number of goroutines. ## 0.5.0 * Added segment timing methods to `Transaction`. These methods must only be used in a single goroutine. * The license length check will not be performed in `Development` mode. * Rename `SetLogFile` to `SetFile` to reduce redundancy. * Added `DebugEnabled` logging guard to reduce overhead. * `Transaction` now implements an `Ignore` method which will prevent any of the transaction's data from being recorded. * `Transaction` now implements a subset of the interfaces `http.CloseNotifier`, `http.Flusher`, `http.Hijacker`, and `io.ReaderFrom` to match the behavior of its wrapped `http.ResponseWriter`. * Changed project name from `go-sdk` to `go-agent`. ## 0.4.0 * Queue time support added: if the inbound request contains an `"X-Request-Start"` or `"X-Queue-Start"` header with a unix timestamp, the agent will report queue time metrics. Queue time will appear on the application overview chart. The timestamp may fractional seconds, milliseconds, or microseconds: the agent will deduce the correct units. go-agent-3.42.0/CONTRIBUTING.md000066400000000000000000000065501510742411500155400ustar00rootroot00000000000000# Contributing Contributions are always welcome. Before contributing please read the [code of conduct](https://opensource.newrelic.com/code-of-conduct/) and [search the issue tracker](../../issues); your issue may have already been discussed or fixed in `master`. To contribute, [fork](https://help.github.com/articles/fork-a-repo/) this repository, commit your changes, and [send a Pull Request](https://help.github.com/articles/using-pull-requests/). Note that our [code of conduct](https://opensource.newrelic.com/code-of-conduct/) applies to all platforms and venues related to this project; please follow it in all your interactions with the project and its participants. Please note, we only accept pull request for versions 3.8.0 of this project or greater. ## Version Support When contributing, keep in mind that New Relic customers are running many different versions of Go, some of them pretty old. For changes that depend on the newest version of Go, the tests will fail and the PR will be rejected. Be aware that the instrumentation needs to work with a wide range of versions of the instrumented modules, and that code that looks nonsensical or overcomplicated may be that way for compatibility-related reasons. Read all the comments and check the related tests before deciding whether existing code is incorrect. If you’re planning on contributing a new feature or an otherwise complex contribution, we kindly ask you to start a conversation with the maintainer team by opening up a GitHub issue first. ## Feature Requests Feature requests should be submitted in the [Issue tracker](../../issues), with a description of the expected behavior & use case, where they’ll remain closed until sufficient interest, [e.g. :+1: reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/), has been [shown by the community](../../issues?q=is%3Aissue+sort%3Areactions-%2B1-desc). Before submitting an Issue, please search for similar ones in the [closed issues](../../issues?q=is%3Aissue+is%3Aclosed). ## Pull Requests Pull requests must pass all automated tests and must be reviewed by at least one maintaining engineer before being merged. Please contribute all pull requests against the `develop` branch, which is where we stage changes ahead of a release and run our most complete suite of tests. When contributing a new integration package, please follow the [Writing a New Integration Package](https://github.com/newrelic/go-agent/wiki/Writing-a-New-Integration-Package) wiki page. ## Contributor License Agreement Keep in mind that when you submit your Pull Request, you'll need to sign the CLA via the click-through using CLA-Assistant. If you'd like to execute our corporate CLA, or if you have any questions, please drop us an email at opensource@newrelic.com. For more information about CLAs, please check out Alex Russell’s excellent post, [“Why Do I Need to Sign This?”](https://infrequently.org/2008/06/why-do-i-need-to-sign-this/). ## Slack We host a public Slack with a dedicated channel for contributors and maintainers of open source projects hosted by New Relic. If you are contributing to this project, you're welcome to request access to the #oss-contributors channel in the newrelicusers.slack.com workspace. To request access, please use this [link](https://join.slack.com/t/newrelicusers/shared_invite/zt-1ayj69rzm-~go~Eo1whIQGYnu3qi15ng). go-agent-3.42.0/Dockerfile000066400000000000000000000011351510742411500152730ustar00rootroot00000000000000# This file is used to build the docker image for the Go Agent's GitHub Action tests ARG GO_VERSION # Takes in go version FROM golang:${GO_VERSION:-1.25} ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ # Convert integration test list to json for GHA jq # Set working directory and run go mod tidy WORKDIR /usr/src/app # Avoid "fatal: detected dubious ownership in repository at 'usr/src/app/'" error # when running git commands inside container with host volume mounted: RUN git config --global --add safe.directory /usr/src/app/ CMD ["bash"] go-agent-3.42.0/GETTING_STARTED.md000066400000000000000000000141171510742411500161160ustar00rootroot00000000000000# Getting Started Follow these steps to instrument your application. More information is available in the [GUIDE.md](GUIDE.md). ## Step 0: Installation The New Relic Go agent is a Go library. It has two dependencies on gRPC libraries - see [go.mod](v3/go.mod). Install the Go agent the same way you would install any other Go library. The simplest way is to run: ``` go get github.com/newrelic/go-agent ``` Then import the package in your application: ```go import "github.com/newrelic/go-agent/v3/newrelic" ``` ## Step 1: Create an Application In your `main` function, or an `init` block, create an [Application](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Application) using [ConfigOptions](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#ConfigOption). Available configurations are listed [here](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Config). [Application](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Application) is the starting point for all instrumentation. ```go func main() { // Create an Application: app, err := newrelic.NewApplication( // Name your application newrelic.ConfigAppName("Your Application Name"), // Fill in your New Relic license key newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), // Add logging: newrelic.ConfigDebugLogger(os.Stdout), // Optional: add additional changes to your configuration via a config function: func(cfg *newrelic.Config) { cfg.CustomInsightsEvents.Enabled = false }, ) // If an application could not be created then err will reveal why. if err != nil { fmt.Println("unable to create New Relic Application", err) } // Now use the app to instrument everything! } ``` Now start your application, and within minutes it will appear in the New Relic UI. Your application in New Relic won't contain much data (until we complete the steps below!), but you will already be able to see a [Go runtime](https://docs.newrelic.com/docs/agents/go-agent/features/go-runtime-page-troubleshoot-performance-problems) page that shows goroutine counts, garbage collection, memory, and CPU usage. ## Step 2: Instrument Requests Using Transactions [Transactions](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Transaction) are used to time inbound requests and background tasks. Use them to see your application's throughput and response time. The instrumentation strategy depends on the framework you're using: #### Standard HTTP Library If you are using the standard library `http` package, use [WrapHandle](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#WrapHandle) and [WrapHandleFunc](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#WrapHandleFunc). As an example, the following code: ```go http.HandleFunc("/users", usersHandler) ``` Can be instrumented like this: ```go http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler)) ``` [Full Example Application](./v3/examples/server/main.go) #### Popular Web Framework If you are using a popular framework, then there may be an integration package designed to instrument it. [List of New Relic Go agent integration packages](./README.md#integrations). #### Manual Transactions If you aren't using the `http` standard library package or an integration package supported framework, you can create transactions directly using the application's `StartTransaction` method: ```go func myHandler(rw http.ResponseWriter, req *http.Request) { txn := h.App.StartTransaction("myHandler") defer txn.End() // Setting the response writer and request is optional. If you don't // set the request, the transaction is considered a background task. txn.SetWebRequestHTTP(req) // Use the ResponseWriter returned in place of the previous ResponseWriter rw = txn.SetWebResponse(rw) rw.Write(data) } ``` Be sure to use a limited set of unique names to ensure that transactions are grouped usefully. Don't use dynamic URLs! [More information about transactions](GUIDE.md#transactions) ## Step 3: Instrument Segments Segments show you where the time in your transactions is being spent. There are four types of segments: [Segment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Segment), [ExternalSegment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#ExternalSegment), [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#DatastoreSegment), and [MessageProducerSegment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#MessageProducerSegment). Creating a segment requires access to the transaction. You can pass the transaction around your functions inside a [context.Context](https://golang.org/pkg/context/#Context) (preferred), or as an explicit transaction parameter of the function. Functions [FromContext](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#FromContext) and [NewContext](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#NewContext) make it easy to store and retrieve the transaction from a context. You may not even need to add the transaction to the context: [WrapHandle](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#WrapHandle) and [WrapHandleFunc](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#WrapHandleFunc) add the transaction to the request's context automatically. ```go func instrumentMe(ctx context.Context) { txn := newrelic.FromContext(ctx) segment := txn.StartSegment("instrumentMe") time.Sleep(1 * time.Second) segment.End() } func myHandler(w http.ResponseWriter, r *http.Request) { instrumentMe(r.Context()) } func main() { app, _ := newrelic.NewApplication( newrelic.ConfigAppName("appName"), newrelic.ConfigLicense("__license__"), ) http.HandleFunc(newrelic.WrapHandleFunc(app, "/handler", myHandler)) } ``` [More information about segments](GUIDE.md#segments) ## Extra Credit Read our [GUIDE.md](GUIDE.md) and the [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic) to learn more about what else you can do with the Go Agent. go-agent-3.42.0/GUIDE.md000066400000000000000000000754341510742411500144750ustar00rootroot00000000000000# New Relic Go Agent Guide * [Upgrading](#upgrading) * [Installation](#installation) * [Full list of `Config` options and `Application` settings](#full-list-of-config-options-and-application-settings) * [Logging](#logging) * [Transactions](#transactions) * [Segments](#segments) * [Datastore Segments](#datastore-segments) * [External Segments](#external-segments) * [Message Producer Segments](#message-producer-segments) * [Attributes](#attributes) * [Tracing](#tracing) * [Distributed Tracing](#distributed-tracing) * [Cross-Application Tracing](#cross-application-tracing) * [Tracing instrumentation](#tracing-instrumentation) * [Getting Tracing Instrumentation Out-of-the-Box](#getting-tracing-instrumentation-out-of-the-box) * [Manually Implementing Distributed Tracing](#manually-implementing-distributed-tracing) * [Distributed Tracing](#distributed-tracing) * [Custom Metrics](#custom-metrics) * [Custom Events](#custom-events) * [Request Queuing](#request-queuing) * [Error Reporting](#error-reporting) * [NoticeError](#noticeerror) * [Panics](#panics) * [Error Response Codes](#error-response-codes) * [Naming Transactions and Metrics](#naming-transactions-and-metrics) * [Browser](#browser) * [For More Help](#for-more-help) ## Upgrading This guide documents version 3.x of the agent which resides in package `"github.com/newrelic/go-agent/v3/newrelic"`. If you have already been using version 2.X of the agent and are upgrading to version 3.0, see our [Migration Guide](MIGRATION.md) for details. ## Installation (Also see [GETTING_STARTED](https://github.com/newrelic/go-agent/blob/master/GETTING_STARTED.md) if you are using the Go agent for the first time). In order to install the New Relic Go agent, you need a New Relic license key. Then, installing the Go Agent is the same as installing any other Go library. The simplest way is to run: ``` go get github.com/newrelic/go-agent/v3/newrelic ``` Then import the package in your application: ```go import "github.com/newrelic/go-agent/v3/newrelic" ``` Initialize the New Relic Go agent by adding the following `Config` options and `Application` settings in the `main` function or in an `init` block: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), ) ``` This will allow you to see Go runtime information. Now, add instrumentation to your Go application to get additional performance data: * Import any of our [integration packages](https://github.com/newrelic/go-agent#integrations) for out-of-the box support for many popular Go web frameworks and libraries. * [Instrument Transactions](#transactions) * [Use Distributed Tracing](#distributed-tracing) (Note that this is on by default.) * [(Optional) Instrument Segments](#segments) for an extra level of timing detail * External segments are needed for Distributed Tracing * Read through the rest of this GUIDE for more instrumentation Compile and deploy your application. Find your application in the New Relic UI. Click on it to see application performance, including the Go runtime page that shows information about goroutine counts, garbage collection, memory, and CPU usage. Data should show up within 5 minutes. If you are working in a development environment or running unit tests, you may not want the Go Agent to spawn goroutines or report to New Relic. You're in luck! Use the `ConfigEnabled` function to disable the agent. This makes the license key optional. ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigEnabled(false), ) ``` ## Full list of `Config` options and `Application` settings * [Config godoc](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Config) * [Application godoc](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application) ## Logging For information about logs in context, please see [the documentation here](https://docs.newrelic.com/docs/logs/logs-context/logs-in-context). As of Go Agent version 3.17.0, we support logs in context with the zerolog integration. The agent's logging system is designed to be easily extensible. By default, no logging will occur. To enable logging, use the following config functions with an [io.Writer](https://godoc.org/github.com/pkg/io/#Writer): [ConfigInfoLogger](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#ConfigInfoLogger), which logs at info level, and [ConfigDebugLogger](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#ConfigDebugLogger) which logs at debug level. To log at debug level to standard out, set: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), // Add debug logging: newrelic.ConfigDebugLogger(os.Stdout), ) ``` To log at info level to a file, set: ```go w, err := os.OpenFile("my_log_file", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if nil == err { app, _ := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), newrelic.ConfigInfoLogger(w), ) } ``` Popular logging libraries `logrus`, `logxi`, `zap` and `zerolog` are supported by integration packages: * [v3/integrations/nrlogrus](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus/) * [v3/integrations/nrlogxi](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi/) * [v3/integrations/nrzap](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap/) * [v3/integrations/nrzerolog](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog/) ## Transactions * [Transaction godoc](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction) * [Naming Transactions](#naming-transactions-and-metrics) * [More info on Transactions](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/transactions-page) Transactions time requests and background tasks. The simplest way to create transactions is to use `Application.StartTransaction` and `Transaction.End`. ```go txn := app.StartTransaction("transactionName") defer txn.End() ``` If you are instrumenting a background transaction, this is all that is needed. If, however, you are instrumenting a web transaction, you will want to use the `SetWebRequestHTTP` and `SetWebResponse` methods as well. [SetWebRequestHTTP](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Transaction.SetWebRequestHTTP) marks the transaction as a web transaction. If the [http.Request](https://godoc.org/net/http#Request) is non-nil, `SetWebRequestHTTP` will additionally collect details on request attributes, url, and method. If headers are present, the agent will look for a distributed tracing header. If you want to mark a transaction as a web transaction, but don't have access to an `http.Request`, you can use the [SetWebRequest](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Transaction.SetWebRequest) method, using a manually constructed [WebRequest](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#WebRequest) object. [SetWebResponse](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#Transaction.SetWebResponse) allows the Transaction to instrument response code and response headers. Pass in your [http.ResponseWriter](https://godoc.org/net/http#ResponseWriter) as a parameter, and then use the return value of this method in place of the input parameter in your instrumentation. Here is an example using both methods: ```go func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { txn := h.App.StartTransaction("transactionName") defer txn.End() // This marks the transaction as a web transactions and collects details on // the request attributes txn.SetWebRequestHTTP(req) // This collects details on response code and headers. Use the returned // Writer from here on. writer = txn.SetWebResponse(writer) // ... handler code continues here using the new writer } ``` The transaction has helpful methods like `NoticeError` and `SetName`. See more in [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction). If you are using [`http.ServeMux`](https://golang.org/pkg/net/http/#ServeMux), use `WrapHandle` and `WrapHandleFunc`. These wrappers automatically start and end transactions with the request and response writer. ```go http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler)) ``` To access the transaction in your handler, we recommend getting it from the Request context: ```go func myHandler(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) // ... handler code here } ``` To monitor a transaction across multiple goroutines, use `Transaction.NewGoroutine()`. The `NewGoroutine` method returns a new reference to the `Transaction`, which is required by each segment-creating goroutine. It does not matter if you call `NewGoroutine` before or after the other goroutine starts. ```go go func(txn newrelic.Transaction) { defer txn.StartSegment("async").End() time.Sleep(100 * time.Millisecond) }(txn.NewGoroutine()) ``` ## Segments Find out where the time in your transactions is being spent! `Segment` is used to instrument functions, methods, and blocks of code. A segment begins when its `StartTime` field is populated, and finishes when its `End` method is called. ```go segment := newrelic.Segment{} segment.Name = "mySegmentName" segment.StartTime = txn.StartSegmentNow() // ... code you want to time here ... segment.End() ``` `Transaction.StartSegment` is a convenient helper. It creates a segment and starts it: ```go segment := txn.StartSegment("mySegmentName") // ... code you want to time here ... segment.End() ``` Timing a function is easy using `StartSegment` and `defer`. Just add the following line to the beginning of that function: ```go defer txn.StartSegment("mySegmentName").End() ``` Segments may be nested. The segment being ended must be the most recently started segment. ```go s1 := txn.StartSegment("outerSegment") s2 := txn.StartSegment("innerSegment") // s2 must be ended before s1 s2.End() s1.End() ``` A zero value segment may safely be ended. Therefore, the following code is safe even if the conditional fails: ```go var s newrelic.Segment txn := newrelic.FromContext(ctx) if shouldDoSomething() { s.StartTime = txn.StartSegmentNow(), } // ... code you wish to time here ... s.End() ``` ### Datastore Segments Datastore segments appear in the transaction "Breakdown table" and in the "Databases" page. * [More info on Databases page](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/databases-slow-queries-page) Datastore segments are instrumented using [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#DatastoreSegment). Just like basic segments, datastore segments begin when the `StartTime` field is populated and finish when the `End` method is called. Here is an example: ```go s := newrelic.DatastoreSegment{ // Product is the datastore type. See the constants in // https://github.com/newrelic/go-agent/blob/master/v3/newrelic/datastore.go. Product // is one of the fields primarily responsible for the grouping of Datastore // metrics. Product: newrelic.DatastoreMySQL, // Collection is the table or group being operated upon in the datastore, // e.g. "users_table". This becomes the db.collection attribute on Span // events and Transaction Trace segments. Collection is one of the fields // primarily responsible for the grouping of Datastore metrics. Collection: "users_table", // Operation is the relevant action, e.g. "SELECT" or "GET". Operation is // one of the fields primarily responsible for the grouping of Datastore // metrics. Operation: "SELECT", } s.StartTime = txn.StartSegmentNow() // ... make the datastore call s.End() ``` This may be combined into two lines when instrumenting a datastore call that spans an entire function call: ```go s := newrelic.DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: newrelic.DatastoreMySQL, Collection: "my_table", Operation: "SELECT", } defer s.End() ``` If you are using the standard library's [database/sql](https://golang.org/pkg/database/sql/) package with [MySQL](https://github.com/go-sql-driver/mysql), [PostgreSQL](https://github.com/lib/pq), or [SQLite](https://github.com/mattn/go-sqlite3) then you can avoid creating DatastoreSegments by hand by using an integration package: * [v3/integrations/nrpq](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq) * [v3/integrations/nrmysql](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql) * [v3/integrations/nrsqlite3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsqlite3) * [v3/integrations/nrmongo](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo) ### External Segments External segments appear in the transaction "Breakdown table" and in the "External services" page. Version 1.11.0 of the Go Agent adds support for cross-application tracing (CAT), which will result in external segments also appearing in the "Service maps" page and being linked in transaction traces when both sides of the request have traces. Version 2.1.0 of the Go Agent adds support for distributed tracing, which lets you see the path a request takes as it travels through distributed APM apps. * [More info on External Services page](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/external-services-page) * [More info on Cross-Application Tracing](https://docs.newrelic.com/docs/apm/transactions/cross-application-traces/introduction-cross-application-traces) * [More info on Distributed Tracing](https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/introduction-distributed-tracing) External segments are instrumented using `ExternalSegment`. There are three ways to use this functionality: 1. Using `StartExternalSegment` to create an `ExternalSegment` before the request is sent, and then calling `ExternalSegment.End` when the external request is complete. For CAT support to operate, an `http.Request` must be provided to `StartExternalSegment`, and the `ExternalSegment.Response` field must be set before `ExternalSegment.End` is called or deferred. For example: ```go func external(txn *newrelic.Transaction, req *http.Request) (*http.Response, error) { s := newrelic.StartExternalSegment(txn, req) response, err := http.DefaultClient.Do(req) s.Response = response s.End() return response, err } ``` If the transaction is `nil` then `StartExternalSegment` will look for a transaction in the request's context using [FromContext](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#FromContext). 2. Using `NewRoundTripper` to get a [`http.RoundTripper`](https://golang.org/pkg/net/http/#RoundTripper) that will automatically instrument all requests made via [`http.Client`](https://golang.org/pkg/net/http/#Client) instances that use that round tripper as their `Transport`. This option results in CAT support, provided the Go Agent is version 1.11.0, and in distributed tracing support, provided the Go Agent is version 2.1.0. `NewRoundTripper` will look for a transaction in the request's context using [FromContext](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#FromContext). For example: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) request, _ := http.NewRequest("GET", "https://example.com", nil) // Put transaction in the request's context: request = newrelic.RequestWithTransactionContext(request, txn) resp, err := client.Do(request) ``` 3. Directly creating an `ExternalSegment` via a struct literal with an explicit `URL` or `Request`, and then calling `ExternalSegment.End`. This option does not support CAT, and may be removed or changed in a future major version of the Go Agent. As a result, we suggest using one of the other options above wherever possible. For example: ```go func external(txn newrelic.Transaction, url string) (*http.Response, error) { es := newrelic.ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: url, } defer es.End() return http.Get(url) } ``` ### Message Producer Segments Message producer segments appear in the transaction "Breakdown table". Message producer segments are instrumented using [MessageProducerSegment](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/#MessageProducerSegment). Just like basic segments, message producer segments begin when the `StartTime` field is populated and finish when the `End` method is called. Here is an example: ```go s := newrelic.MessageProducerSegment{ // Library is the name of the library instrumented. Library: "RabbitMQ", // DestinationType is the destination type. DestinationType: newrelic.MessageExchange, // DestinationName is the name of your queue or topic. DestinationName: "myExchange", // DestinationTemporary must be set to true if destination is temporary // to improve metric grouping. DestinationTemporary: false, } s.StartTime = txn.StartSegmentNow() // ... add message to queue here s.End() ``` This may be combined into two lines when instrumenting a message producer call that spans an entire function call: ```go s := newrelic.MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: newrelic.MessageExchange, DestinationName: "myExchange", DestinationTemporary: false, } defer s.End() ``` ## Attributes Attributes add context to errors and allow you to filter performance data in Insights. You may add them using the `Transaction.AddAttribute` and `Segment.AddAttribute` methods. ```go txn.AddAttribute("key", "value") txn.AddAttribute("product", "widget") txn.AddAttribute("price", 19.99) txn.AddAttribute("importantCustomer", true) seg.AddAttribute("count", 14) ``` * [More info on Custom Attributes](https://docs.newrelic.com/docs/insights/new-relic-insights/decorating-events/insights-custom-attributes) Some attributes are recorded automatically. These are called agent attributes. They are listed here: * [newrelic package constants](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#pkg-constants) To disable one of these agents attributes, for example `AttributeHostDisplayName`, modify the config like this: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), func(cfg *newrelic.Config) { config.Attributes.Exclude = append(config.Attributes.Exclude, newrelic.AttributeHostDisplayName) } ) ``` * [More info on Agent Attributes](https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/agent-attributes) ## Tracing New Relic's [distributed tracing](https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/introduction-distributed-tracing) is the next generation of the previous cross-application tracing feature. Compared to cross-application tracing, distributed tracing gives more detail about cross-service activity and provides more complete end-to-end visibility. This section discusses distributed tracing and cross-application tracing in turn. ### Distributed Tracing New Relic's [distributed tracing](https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/introduction-distributed-tracing) feature lets you see the path that a request takes as it travels through distributed APM apps, which is vital for applications implementing a service-oriented or microservices architecture. Support for distributed tracing was added in version 2.1.0 of the Go Agent. The config's `DistributedTracer.Enabled` field has to be set. When true, the agent will add distributed tracing headers in outbound requests, and scan incoming requests for distributed tracing headers. Distributed tracing will override cross-application tracing. ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), newrelic.ConfigDistributedTracerEnabled(true), ) ``` ### Cross-Application Tracing [Deprecated] New Relic's [cross-application tracing](https://docs.newrelic.com/docs/apm/transactions/cross-application-traces/introduction-cross-application-traces) feature, or CAT for short, links transactions between applications in APM to help identify performance problems within your service-oriented architecture. Support for CAT was added in version 1.11.0 of the Go Agent. We recommend using [Distributed Tracing](#distributed-tracing) as the most recent, complete feature. As CAT uses HTTP headers to track requests across applications, the Go Agent needs to be able to access and modify request and response headers both for incoming and outgoing requests. ### Tracing Instrumentation Both distributed tracing and cross-application tracing work by propagating [header information](https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/how-new-relic-distributed-tracing-works#headers) from service to service in a request path. In many scenarios, the Go Agent offers tracing instrumentation out-of-the-box, for both distributed tracing and cross-application tracing. For other scenarios customers may implement distributed tracing based on the examples provided in this guide. #### Getting Tracing Instrumentation Out-of-the-Box The Go Agent automatically creates and propagates tracing header information for each of the following scenarios: For server applications: 1. Using `WrapHandle` or `WrapHandleFunc` to instrument a server that uses [`http.ServeMux`](https://golang.org/pkg/net/http/#ServeMux) ([Example](v3/examples/server/main.go)). 2. Using any of the Go Agent's HTTP integrations, which are listed [here ](README.md#integrations). 3. Using another framework or [`http.Server`](https://golang.org/pkg/net/http/#Server) while ensuring that: 1. After calling `StartTransaction`, make sure to call `Transaction.SetWebRequest` and `Transaction.SetWebResponse` on the transaction, and 2. the `http.ResponseWriter` that is returned from `Transaction.SetWebResponse` is used instead of calling `WriteHeader` directly on the original response writer, as described in the [transactions section of this guide](#transactions) ([Example](v3/examples/server-http/main.go)). For client applications: 1. Using `NewRoundTripper`, as described in the [external segments section of this guide](#external-segments) ([Example](v3/examples/client-round-tripper/main.go)). 2. Using the call `StartExternalSegment` and providing an `http.Request`, as described in the [external segments section of this guide](#external-segments) ([Example](v3/examples/client/main.go)). #### Manually Implementing Distributed Tracing Consider [manual instrumentation](https://docs.newrelic.com/docs/apm/distributed-tracing/enable-configure/enable-distributed-tracing#agent-apis) for services not instrumented automatically by the Go Agent. In such scenarios, the calling service has to insert the appropriate header(s) into the request headers: ```go var h http.Headers callingTxn.InsertDistributedTraceHeaders(h) ``` These headers have to be added to the call to the destination service, which in turn invokes the call for accepting the headers: ```go var h http.Headers calledTxn.AcceptDistributedTraceHeaders(newrelic.TransportOther, h) ``` A complete example can be found [here](v3/examples/custom-instrumentation/main.go). ## Custom Metrics * [More info on Custom Metrics](https://docs.newrelic.com/docs/agents/go-agent/instrumentation/create-custom-metrics-go) You may [create custom metrics](https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/collect-custom-metrics) via the `RecordCustomMetric` method. ```go app.RecordCustomMetric( "CustomMetricName", // Name of your metric 132, // Value ) ``` **Note:** The Go Agent will automatically prepend the metric name you pass to `RecordCustomMetric` (`"CustomMetricName"` above) with the string `Custom/`. This means the above code would produce a metric named `Custom/CustomMetricName`. You'll also want to read over the [Naming Transactions and Metrics](#naming-transactions-and-metrics) section below for advice on coming up with appropriate metric names. ## Custom Events You may track arbitrary events using custom Insights events. ```go app.RecordCustomEvent("MyEventType", map[string]interface{}{ "myString": "hello", "myFloat": 0.603, "myInt": 123, "myBool": true, }) ``` ## Request Queuing If you are running a load balancer or reverse web proxy then you may configure it to add a `X-Queue-Start` header with a Unix timestamp. This will create a band on the application overview chart showing queue time. * [More info on Request Queuing](https://docs.newrelic.com/docs/apm/applications-menu/features/request-queuing-tracking-front-end-time) ## Error Reporting The Go Agent captures errors in three different ways: 1. [the Transaction.NoticeError method](#noticeerror) 2. [panics recovered in defer Transaction.End](#panics) 3. [error response status codes recorded with Transaction.WriteHeader](#error-response-codes) ### NoticeError You may track errors using the `Transaction.NoticeError` method. The easiest way to get started with `NoticeError` is to use errors based on [Go's standard error interface](https://blog.golang.org/error-handling-and-go). ```go txn.NoticeError(errors.New("my error message")) ``` `NoticeError` will work with *any* sort of object that implements Go's standard error type interface -- not just `errorStrings` created via `errors.New`. If you're interested in sending more than an error *message* to New Relic, the Go Agent also offers a `newrelic.Error` struct. ```go txn.NoticeError(newrelic.Error{ Message: "my error message", Class: "IdentifierForError", Attributes: map[string]interface{}{ "important_number": 97232, "relevant_string": "zap", }, }) ``` Using the `newrelic.Error` struct requires you to manually marshal your error data into the `Message`, `Class`, and `Attributes` fields. However, there's two **advantages** to using the `newrelic.Error` struct. First, by setting an error `Class`, New Relic will be able to aggregate errors in the *Error Analytics* section of APM. Second, the `Attributes` field allows you to send through key/value pairs with additional error debugging information (also exposed in the *Error Analytics* section of APM). ### Panics When the Transaction is ended using `defer`, the Transaction will optionally recover any panic that occurs, record it as an error, and re-throw it. You can enable this feature by setting the configuration: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), func(cfg *newrelic.Config) { cfg.ErrorCollector.RecordPanics = true } ) ``` As a result of this configuration, panics may appear to be originating from `Transaction.End`. ```go func unstableTask(app newrelic.Application) { txn := app.StartTransaction("unstableTask", nil, nil) defer txn.End() // This panic will be recorded as an error. panic("something went wrong") } ``` ### Error Response Codes Setting the WebResponse on the transaction using `Transaction.SetWebResponse` returns an [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter), and you can use that returned ResponseWriter to call `WriteHeader` to record the response status code. The transaction will record an error if the status code is at or above 400 or strictly below 100 and not in the ignored status codes configuration list. The ignored status codes list is configured by the `Config.ErrorCollector.IgnoreStatusCodes` field or within the New Relic UI if your application has server side configuration enabled. As a result, using `Transaction.NoticeError` in situations where your code is returning an erroneous status code may result in redundant errors. `NoticeError` is not affected by the ignored status codes configuration list. ## Naming Transactions and Metrics You'll want to think carefully about how you name your transactions and custom metrics. If your program creates too many unique names, you may end up with a [Metric Grouping Issue (or MGI)](https://docs.newrelic.com/docs/agents/manage-apm-agents/troubleshooting/metric-grouping-issues). MGIs occur when the granularity of names is too fine, resulting in hundreds or thousands of uniquely identified metrics and transactions. One common cause of MGIs is relying on the full URL name for metric naming in web transactions. A few major code paths may generate many different full URL paths to unique documents, articles, page, etc. If the unique element of the URL path is included in the metric name, each of these common paths will have its own unique metric name. ## Browser To enable support for [New Relic Browser](https://docs.newrelic.com/docs/browser), your HTML pages must include a JavaScript snippet that will load the Browser agent and configure it with the correct application name. This snippet is available via the `Transaction.BrowserTimingHeader` method. Include the byte slice returned by `Transaction.BrowserTimingHeader().WithTags()` as early as possible in the `` section of your HTML after any `` tags. ```go func indexHandler(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "") // The New Relic browser javascript should be placed as high in the // HTML as possible. We suggest including it immediately after the // opening tag and any tags. txn := newrelic.FromContext(req.Context()) hdr, err := txn.BrowserTimingHeader() if nil != err { log.Printf("unable to create browser timing header: %v", err) } // BrowserTimingHeader() will always return a header whose methods can // be safely called. if js := hdr.WithTags(); js != nil { w.Write(js) } io.WriteString(w, "browser header page") } ``` ## For More Help There's a variety of places online to learn more about the Go Agent. [The New Relic docs site](https://docs.newrelic.com/docs/agents/go-agent/get-started/introduction-new-relic-go) contains a number of useful code samples and more context about how to use the Go Agent. [New Relic's discussion forums](https://discuss.newrelic.com) have a dedicated public forum [for the Go Agent](https://discuss.newrelic.com/c/support-products-agents/go-agent). When in doubt, [the New Relic support site](https://support.newrelic.com/) is the best place to get started troubleshooting an agent issue. go-agent-3.42.0/LICENSE.txt000066400000000000000000000264501510742411500151330ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/MIGRATION.md000066400000000000000000000670031510742411500151620ustar00rootroot00000000000000# Migration Guide - 3.0 This guide is intended to help with upgrading from version 2.x (`"github.com/newrelic/go-agent"`) to version 3.x (`"github.com/newrelic/go-agent/v3/newrelic"`). This information can also be found on [our documentation website](https://docs.newrelic.com/docs/agents/go-agent/installation/update-go-agent). * [List of all changes](#all-changes) * [Checklist for upgrading](#checklist-for-upgrading) ## All Changes ### Dropped support for Go versions < 1.7 The minimum required Go version to run the New Relic Go Agent is now 1.7. ### Package names The agent has been placed in a new `/v3` directory, leaving the top level directory with the now deprecated v2 agent. More specifically: * The `newrelic` package has moved from `"github.com/newrelic/go-agent"` to `"github.com/newrelic/go-agent/v3/newrelic"`. This makes named imports unnecessary. * The underscore in the `_integrations` directory is removed. Thus the `"github.com/newrelic/go-agent/_integrations/nrlogrus"` import path becomes `"github.com/newrelic/go-agent/v3/integrations/nrlogrus"`. Some of the integration packages have had other changes as well: * `_integrations/nrawssdk/v1` moves to `v3/integrations/nrawssdk-v1` * `_integrations/nrawssdk/v2` moves to `v3/integrations/nrawssdk-v2` * `_integrations/nrgin/v1` moves to `v3/integrations/nrgin` * `_integrations/nrgorilla/v1` moves to `v3/integrations/nrgorilla` * `_integrations/nrlogxi/v1` moves to `v3/integrations/nrlogxi` * `_integrations/nrecho` moves to `v3/integrations/nrecho-v3` and a new `v3/integrations/nrecho-v4` has been added to support Echo version 4. ### Transaction Name Changes Transaction names created by [`WrapHandle`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandle), [`WrapHandleFunc`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandleFunc), [nrecho-v3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3), [nrecho-v4](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4), [nrgorilla](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla), and [nrgin](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin) now include the HTTP method. For example, the following code: ```go http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler)) ``` now creates a metric called `WebTransaction/Go/GET /users` instead of `WebTransaction/Go/users`. **As a result of this change, you may need to update your alerts and dashboards.** ### Go modules We have added go module support. The top level `"github.com/newrelic/go-agent/v3/newrelic"` package now has a `go.mod` file. Separate `go.mod` files are also included with each integration in the integrations directory. ### Configuration `NewConfig` was removed and the [`NewApplication`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewApplication) signature has changed to: ```go func NewApplication(opts ...ConfigOption) (*Application, error) ````` New [`ConfigOption`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigOption) functions are provided to modify the [`Config`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Config). Here's what your Application creation will look like: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("My Application"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), ) ``` A complete list of `ConfigOption`s can be found in the [Go Docs](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigOption). ### Config.TransactionTracer The location of two [`Config`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Config) fields have been moved. The `Config.TransactionTracer.SegmentThreshold` field has moved to `Config.TransactionTracer.Segments.Threshold` and the `Config.TransactionTracer.StackTraceThreshold` field has moved to to `Config.TransactionTracer.Segments.StackTraceThreshold`. ### Remove API error return values The following method signatures have changed to no longer return an error; instead the error is logged to the agent logs. ```go func (txn *Transaction) End() {...} func (txn *Transaction) Ignore() {...} func (txn *Transaction) SetName(name string) {...} func (txn *Transaction) NoticeError(err error) {...} func (txn *Transaction) AddAttribute(key string, value interface{}) {...} func (txn *Transaction) SetWebRequestHTTP(r *http.Request) {...} func (txn *Transaction) SetWebRequest(r *WebRequest) {...} func (txn *Transaction) AcceptDistributedTracePayload(t TransportType, payload interface{}) {...} func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader {...} func (s *Segment) End() {...} func (s *DatastoreSegment) End() {...} func (s *ExternalSegment) End() {...} func (s *MessageProducerSegment) End() {...} func (app *Application) RecordCustomEvent(eventType string, params map[string]interface{}) {...} func (app *Application) RecordCustomMetric(name string, value float64) {...} ``` ### `Application.StartTransaction` signature change The signature of [`Application.StartTransaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application.StartTransaction) has changed to no longer take a `http.ResponseWriter` or `*http.Request`. The new signature just takes a string for the transaction name: ```go func (app *Application) StartTransaction(name string) *Transaction ``` If you previously had code that used all three parameters, such as: ```go var writer http.ResponseWriter var req *http.Request txn := h.App.StartTransaction("server-txn", writer, req) ``` After the upgrade, it should look like this: ```go var writer http.ResponseWriter var req *http.Request txn := h.App.StartTransaction("server-txn") writer = txn.SetWebResponse(writer) txn.SetWebRequestHTTP(req) ``` ### Application and Transaction [`Application`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application) and [`Transaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction) have changed from interfaces to structs. All methods on these types have pointer receivers. Methods on these types are now nil-safe. References to these types in your code will need a pointer added. See the [checklist](#checklist-for-upgrading) for examples. ### Renamed attributes Two attributes have been renamed. The old names will still be reported, but are deprecated and will be removed entirely in a future release. | Old (deprecated) attribute | New attribute | |------------------------------|------------------------------| | `httpResponseCode` | `http.statusCode` | | `request.headers.User-Agent` | `request.headers.userAgent` | Since in v3.0 both the deprecated and the new attribute are being reported, if you have configured your application to ignore one or both of these attributes, such as with `Config.Attributes.Exclude`, you will now need to specify both the deprecated and the new attribute name in your configuration. ### RecordPanics configuration option This version introduces a new configuration option, `Config.ErrorCollector.RecordPanics`. This configuration controls whether or not a deferred `Transaction.End` will attempt to recover panics, record them as errors, and then re-panic them. By default, this is set to `false`. Previous versions of the agent always recovered panics, i.e. a default of `true`. ### New config option for getting data from environment variables Along with the new format for configuring an application, there is now an option to populate the configuration from environment variables. The full list of environment variables that are considered are included in the [Go Docs](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigFromEnvironment). The new configuration function is used as follows: ```go app, err := newrelic.NewApplication(newrelic.ConfigFromEnvironment()) ``` ### `Transaction` no longer implements `http.ResponseWriter`. As mentioned above, the [`Application.StartTransaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application.StartTransaction) no longer takes a `http.ResponseWriter` or `http.Request`; instead, after you start the transaction, you can set the `ResponseWriter` by calling [`Transaction.SetWebResponse`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebResponse): ```go txn := h.App.StartTransaction("server-txn") writer = txn.SetWebResponse(writer) ``` The [`Transaction.SetWebResponse`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebResponse) method now returns a replacement `http.ResponseWriter` that implements the combination of `http.CloseNotifier`, `http.Flusher`, `http.Hijacker`, and `io.ReaderFrom` implemented by the input `http.ResponseWriter`. ### The `WebRequest` type has changed from an interface to a struct [`WebRequest`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WebRequest) has changed from an interface to a struct, which can be created via code like this: ```go webReq := newrelic.WebRequest{ Header: hdrs, URL: url, Method: method, Transport: newrelic.TransportHTTP, } ``` The [`Transaction.SetWebRequest`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebRequest) method takes one of these structs. ### `SetWebRequestHTTP` method added In addition to the [`Transaction.SetWebRequest`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebRequest) method discussed in the section above, we have added a method [`Transaction.SetWebRequestHTTP`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebRequestHTTP) that takes an `*http.Request` and sets the appropriate fields. As described in the [earlier section](#applicationstarttransaction-signature-change), this can be used in your code as part of the signature change of [`Application.StartTransaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application.StartTransaction): ```go var writer http.ResponseWriter var req *http.Request txn := h.App.StartTransaction("server-txn") writer = txn.SetWebResponse(writer) txn.SetWebRequestHTTP(req) ``` ### `NewRoundTripper` The transaction parameter to [`NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewRoundTripper) has been removed. The function signature is now: ```go func NewRoundTripper(t http.RoundTripper) http.RoundTripper ``` [`NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewRoundTripper) will look for a transaction in the request's context using [`FromContext`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#FromContext). ### Distributed Trace methods When manually creating or accepting Distributed Tracing payloads, the method signatures have changed. This [`Transaction.InsertDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.InsertDistributedTraceHeaders) method will insert the Distributed Tracing headers into the `http.Header` object passed as a parameter: ```go func (txn *Transaction) InsertDistributedTraceHeaders(hdrs http.Header) ``` This [`Transaction.AcceptDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.AcceptDistributedTraceHeaders) method takes a [`TransportType`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#TransportType) and an `http.Header` object that contains Distributed Tracing header(s) and links this transaction to other transactions specified in the headers: ```go func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http.Header) ``` Additionally, the `DistributedTracePayload` struct is no longer needed and has been removed from the agent's API. Instead, distributed tracing information is passed around as key/value pairs in the `http.Header` object. ### Several functions marked as deprecated The functions [`StartSegmentNow`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#StartSegmentNow) and [`StartSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#StartSegment) have been marked as deprecated. The preferred new method of starting a segment have moved to [`Transaction.StartSegmentNow`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.StartSegmentNow) and [`Transaction.StartSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.StartSegment) respectively. ```go // DEPRECATED: startTime := newrelic.StartSegmentNow(txn) // and sgmt := newrelic.StartSegment(txn, "segment1") ``` ```go // NEW, PREFERRED WAY: startTime := txn.StartSegmentNow() // and sgmt := txn.StartSegment("segment1") ``` Additionally the functions [`NewLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewLogger) and [`NewDebugLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewDebugLogger) have been marked as deprecated. The preferred new method of configuring agent logging is using the [`ConfigInfoLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigInfoLogger) and [`ConfigDebugLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigDebugLogger) `ConfigOptions` respectively. ```go // DEPRECATED: app, err := newrelic.NewApplication( ... func(cfg *newrelic.Config) { cfg.Logger = newrelic.NewLogger(os.Stdout) } ) // or app, err := newrelic.NewApplication( ... func(cfg *newrelic.Config) { cfg.Logger = newrelic.NewDebugLogger(os.Stdout) } ) ``` ```go // NEW, PREFERRED WAY: app, err := newrelic.NewApplication( ... newrelic.ConfigInfoLogger(os.Stdout), ) // or app, err := newrelic.NewApplication( ... newrelic.ConfigDebugLogger(os.Stdout), ) ``` ### Removed optional interfaces from error The interfaces `ErrorAttributer`, `ErrorClasser`, `StackTracer` are no longer exported. Thus, if you have any code checking to ensure that your custom type fulfills these interfaces, that code will no longer work. Example: ```go // This will no longer compile. type MyErrorType struct{} var _ newrelic.ErrorAttributer = MyErrorType{} ``` ### Changed Distributed Tracing Constant `DistributedTracePayloadHeader` has been changed to `DistributedTraceNewRelicHeader`. ### `TransportType` [`TransportType`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#TransportType) type is changed from a struct to a string. ## Checklist for upgrading - [ ] Ensure your Go version is at least 1.7 (older versions are no longer supported). - [ ] Update imports. The v3.x agent now lives at "github.com/newrelic/go-agent/v3/newrelic" and no longer requires a named import. From: ```go import newrelic "github.com/newrelic/go-agent" ``` To: ```go import "github.com/newrelic/go-agent/v3/newrelic" ``` Additionally, if you are using any integrations, they too have moved. Each has its own version which matches the version of the 3rd party package it supports. From: ```go import "github.com/newrelic/go-agent/_integrations/nrlogrus" ``` To: ```go import "github.com/newrelic/go-agent/v3/integrations/nrlogrus" ``` - [ ] Update how you configure your application. The [`NewApplication`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewApplication) function now accepts [`ConfigOption`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigOption)s a list of which can be [found here](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigOption). If a [`ConfigOption`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigOption) is not available for your setting, create one yourself! From: ```go cfg := newrelic.NewConfig("appName", "__license__") cfg.CrossApplicationTracer.Enabled = false cfg.CustomInsightsEvents.Enabled = false cfg.ErrorCollector.IgnoreStatusCodes = []int{404, 418} cfg.DatastoreTracer.SlowQuery.Threshold = 3 cfg.DistributedTracer.Enabled = true cfg.TransactionTracer.Threshold.Duration = 2 cfg.TransactionTracer.Threshold.IsApdexFailing = false app, err := newrelic.NewApplication(cfg) ```` To: ```go app, err := newrelic.NewApplication( newrelic.ConfigAppName("appName"), newrelic.ConfigLicense("__license__"), func(cfg *newrelic.Config) { cfg.CrossApplicationTracer.Enabled = false cfg.CustomInsightsEvents.Enabled = false cfg.ErrorCollector.IgnoreStatusCodes = []int{404, 418} cfg.DatastoreTracer.SlowQuery.Threshold = 3 cfg.DistributedTracer.Enabled = true cfg.TransactionTracer.Threshold.Duration = 2 cfg.TransactionTracer.Threshold.IsApdexFailing = false }, ) ``` You can use [`ConfigFromEnvironment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigFromEnvironment) to provide configuration from environment variables: ```go app, err := newrelic.NewApplication(newrelic.ConfigFromEnvironment()) ``` - [ ] Update the Transaction Tracer configuration. Change the fields for the two changed configuration options. | Old Config Field | New Config Field | |------------------------------------------------|---------------------------------------------------------| | `Config.TransactionTracer.SegmentThreshold` | `Config.TransactionTracer.Segments.Threshold` | | `Config.TransactionTracer.StackTraceThreshold` | `Config.TransactionTracer.Segments.StackTraceThreshold` | - [ ] If you choose, set the `Config.ErrorCollector.RecordPanics` configuration option. This is a new configuration option that controls whether or not a deferred [`Transaction.End`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.End) will attempt to recover panics, record them as errors, and then re-panic them. Previously, the agent acted as though this option was set to `true`; with the new configuration it defaults to `false`. If you wish to maintain the old agent behavior with regards to panics, be sure to set this to `true`. - [ ] Update code to use the new [`Application.StartTransaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application.StartTransaction) signature. From: ```go txn := app.StartTransaction("name", nil, nil) // or // writer is an http.ResponseWriter // req is an *http.Request txn := app.StartTransaction("name", writer, req) txn.WriteHeader(500) ``` To, respectively: ```go txn := app.StartTransaction("name") // or // writer is an http.ResponseWriter // req is an *http.Request txn:= app.StartTransaction("name") writer = txn.SetWebResponse(writer) txn.SetWebRequestHTTP(req) writer.WriteHeader(500) ``` Notice too that the [`Transaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction) no longer fulfills the `http.ResponseWriter` interface. Instead, the writer returned from [`Transaction.SetWebResponse`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.SetWebResponse) should be used. - [ ] Update code to no longer expect an error returned from these updated methods. Instead, check the agent logs for errors by using one of the [`ConfigLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigLogger), [`ConfigInfoLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigInfoLogger), or [`ConfigDebugLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigDebugLogger) configuration options. ```go func (txn *Transaction) End() {...} func (txn *Transaction) Ignore() {...} func (txn *Transaction) SetName(name string) {...} func (txn *Transaction) NoticeError(err error) {...} func (txn *Transaction) AddAttribute(key string, value interface{}) {...} func (txn *Transaction) SetWebRequestHTTP(r *http.Request) {...} func (txn *Transaction) SetWebRequest(r *WebRequest) {...} func (txn *Transaction) AcceptDistributedTracePayload(t TransportType, payload interface{}) {...} func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader {...} func (s *Segment) End() {...} func (s *DatastoreSegment) End() {...} func (s *ExternalSegment) End() {...} func (s *MessageProducerSegment) End() {...} func (app *Application) RecordCustomEvent(eventType string, params map[string]interface{}) {...} func (app *Application) RecordCustomMetric(name string, value float64) {...} ``` - [ ] Update uses of [`Application`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Application) and [`Transaction`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction) to be pointers, instead of direct references. From: ```go func doSomething(txn newrelic.Transaction) {...} func instrumentSomething(app newrelic.Application, h http.Handler, name string) {...} ``` To: ```go func doSomething(txn *newrelic.Transaction) {...} func instrumentSomething(app *newrelic.Application, h http.Handler, name string) {...} ``` - [ ] If you are using a [`WebRequest`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WebRequest) type, it has changed from an interface to a struct. You can use it as follows: ```go wr := newrelic.WebRequest{ Header: r.Header, URL: r.URL, Method: r.Method, Transport: newrelic.TransportHTTP, } txn.SetWebRequest(wr) ``` - [ ] Remove the `Transaction` parameter from the [`NewRoundTripper`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewRoundTripper), and instead ensure that the transaction is available via the request's context, using [`RequestWithTransactionContext`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#RequestWithTransactionContext). From: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(txn, client.Transport) req, _ := http.NewRequest("GET", "https://example.com", nil) client.Do(req) ``` To: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) req, _ := http.NewRequest("GET", "http://example.com", nil) req = newrelic.RequestWithTransactionContext(req, txn) client.Do(req) ``` - [ ] Update any usage of Distributed Tracing accept/create functions. The method for creating a distributed trace payload has changed to [`Transaction.InsertDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.InsertDistributedTraceHeaders). Instead of returning a payload, it now accepts an `http.Header` and inserts the header(s) directly into it. From: ```go hdrs := http.Header{} payload := txn.CreateDistributedTracePayload() hdrs.Set(newrelic.DistributedTracePayloadHeader, payload.Text()) ``` To: ```go hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) ``` Similarly, the method for accepting distributed trace payloads has changed to [`Transaction.AcceptDistributedTraceHeaders`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.AcceptDistributedTraceHeaders). Instead of taking an interface representing the payload value, it now accepts an `http.Header` representing both the keys and values. From: ```go hdrs := request.Headers() payload := hdrs.Get(newrelic.DistributedTracePayloadHeader) txn.AcceptDistributedTracePayload(newrelic.TransportKafka, payload) ``` To: ```go hdrs := request.Headers() txn.AcceptDistributedTraceHeaders(newrelic.TransportKafka, hdrs) ``` Additionally, the `DistributedTracePayload` struct is no longer needed and has been removed from the agent's API. Instead, distributed tracing information is passed around as key/value pairs in the `http.Header` object. You should remove all references to `DistributedTracePayload` in your code. - [ ] Change `newrelic.DistributedTracePayloadHeader` to `newrelic.DistributedTraceNewRelicHeader`. - [ ] If you have configured your application to **ignore** either attribute described [here](#renamed-attributes), you will now need to specify both the deprecated and the new attribute name in your configuration. Configuration options where these codes might be used: ```go Config.TransactionEvents.Attributes Config.ErrorCollector.Attributes Config.TransactionTracer.Attributes Config.TransactionTrace.Segments.Attributes Config.BrowserMonitoring.Attributes Config.SpanEvents.Attributes Config.Attributes ``` From old configuration example: ```go config.ErrorCollector.Attributes.Exclude = []string{ "httpResponseCode", "request.headers.User-Agent", } // or config.ErrorCollector.Attributes.Exclude = []string{ newrelic.AttributeResponseCode, newrelic.AttributeRequestUserAgent, } ``` To: ```go config.ErrorCollector.Attributes.Exclude = []string{ "http.statusCode", "httpResponseCode", "request.headers.userAgent", "request.headers.User-Agent", } // or config.ErrorCollector.Attributes.Exclude = []string{ newrelic.AttributeResponseCode, newrelic.AttributeResponseCodeDeprecated, newrelic.AttributeRequestUserAgent, newrelic.AttributeRequestUserAgentDeprecated, } ``` - [ ] Update alerts and dashboards with new transaction names: Transaction names created by [`WrapHandle`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandle), [`WrapHandleFunc`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandleFunc), [nrecho-v3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3), [nrecho-v4](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4), [nrgorilla](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla), and [nrgin](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin) now include the HTTP method. Thus the transaction name `WebTransaction/Go/users` becomes `WebTransaction/Go/GET /users`. - [ ] Not required for upgrade, but recommended: update your usages of the now deprecated [`StartSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#StartSegment) and [`StartSegmentNow`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#StartSegmentNow) to use the methods on the transaction: [`Transaction.StartSegment`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.StartSegment) and [`Transaction.StartSegmentNow`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Trnasaction.StartSEgmentNow) respectively. This step is optional but highly recommended. From: ```go startTime := newrelic.StartSegmentNow(txn) // and sgmt := newrelic.StartSegment(txn, "segment1") ``` To: ```go startTime := txn.StartSegmentNow() // and sgmt := txn.StartSegment("segment1") ``` - [ ] Not required for upgrade, but recommended: update your usages of the now deprecated [`NewLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewLogger) and [`NewDebugLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#NewDebugLogger). Instead use the new `ConfigOption`s [`ConfigInfoLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigInfoLogger) and [`ConfigDebugLogger`](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#ConfigDebugLogger) respectively. From: ```go app, err := newrelic.NewApplication( ... func(cfg *newrelic.Config) { cfg.Logger = newrelic.NewLogger(os.Stdout) } ) // or app, err := newrelic.NewApplication( ... func(cfg *newrelic.Config) { cfg.Logger = newrelic.NewDebugLogger(os.Stdout) } ) ``` To: ```go app, err := newrelic.NewApplication( ... newrelic.ConfigInfoLogger(os.Stdout), ) // or app, err := newrelic.NewApplication( ... newrelic.ConfigDebugLogger(os.Stdout), ) ``` go-agent-3.42.0/Makefile000066400000000000000000000063721510742411500147510ustar00rootroot00000000000000# # Copyright 2025 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # # The top level Makefile # SHELL = /bin/bash GIT ?= git # Default go invocation command GO := go # module path GO_MODULE := github.com/newrelic/go-agent/v3/newrelic BASEDIR := $(PWD) MODULE_DIR:= ./v3 BENCHTIME := 1ms # Include the secrets file if it exists, but if it doesn't, that's OK too. -include secrets.mk # Include the manifests for the integration and core tests include integration-tests.mk include core-tests.mk # Test targets .PHONY: tidy tidy: @cd $(MODULE_DIR); $(GO) mod edit -replace github.com/newrelic/go-agent/v3="$(BASEDIR)/$(MODULE_DIR)"; $(GO) mod tidy .PHONY: core-test core-test: @echo; echo "# TEST=$(TEST), COVERAGE=$(COVERAGE)"; \ cd $(MODULE_DIR)/$(TEST); \ $(GO) mod edit -replace github.com/newrelic/go-agent/v3="$(BASEDIR)/$(MODULE_DIR)"; \ $(GO) mod tidy; \ if [ "$(COVERAGE)" == "1" ]; then \ $(GO) test -coverprofile=coverage.txt || exit 1; \ else \ $(GO) test || exit 1; \ fi; \ echo "# TEST=$(TEST)"; \ cd $(BASEDIR); .PHONY: core-suite core-suite: @for TEST in $(GO_CORE_TESTS); do \ $(MAKE) core-test TEST=$${TEST} COVERAGE=$(COVERAGE); \ done .PHONY: integration-test integration-test: @echo; echo "# TEST=$(TEST), COVERAGE=$(COVERAGE)"; \ cd $(MODULE_DIR)/integrations/$(TEST); \ WD=$(shell pwd); \ $(GO) mod edit -replace github.com/newrelic/go-agent/v3="$${WD}/${MODULE_DIR}";\ if [ "$(TEST)" == "nrnats" ]; then \ GOPROXY=direct $(GO) mod tidy; \ else \ $(GO) mod tidy; \ fi; \ if [ "$(COVERAGE)" == "1" ]; then \ $(GO) test -coverprofile=coverage.txt -race -benchtime=$(BENCHTIME) -bench=. ./ || exit 1; \ else \ $(GO) test -race -benchtime=$(BENCHTIME) -bench=. ./ || exit 1; \ fi; \ $(GO) vet ./... || exit 1; \ echo "# TEST=$(TEST)"; \ cd $(BASEDIR); .PHONY: integration-suite integration-suite: @for TEST in $(GO_INTEGRATION_TESTS); do \ $(MAKE) integration-test TEST=$${TEST} COVERAGE=$(COVERAGE); \ done test-services-start: @if [ ! -z $(PROFILE) ]; then \ docker compose --profile test --profile $(PROFILE) pull $(SERVICES); \ docker compose --profile test --profile $(PROFILE) up --wait --remove-orphans -d $(SERVICES); \ else \ docker compose --profile test pull $(SERVICES); \ docker compose --profile test up --wait --remove-orphans -d $(SERVICES); \ fi; test-services-stop: @if [ ! -z $(PROFILE) ]; then \ docker compose --profile test --profile $(PROFILE) stop; \ else \ docker compose --profile test stop; \ fi; # Developer targets devenv-image: @docker compose --profile dev build devenv dev-shell: devenv-image docker compose --profile dev up --pull missing --remove-orphans -d docker compose exec -it devenv bash -c "bash" dev-stop: docker compose --profile dev stop # Utility targets .PHONY: integration-to-json integration-to-json: @TESTS="$(shell echo $(GO_INTEGRATION_TESTS))"; \ echo $$TESTS | jq -R 'split(" ")' | sed "s/ //g" | tr -d '\n'; .PHONY: core-to-json core-to-json: @TESTS="$(shell echo $(GO_CORE_TESTS))"; \ echo $$TESTS | jq -R 'split(" ")' | sed "s/ //g" | tr -d '\n'; .PHONY: info info: @echo @echo "$$(go version)" @echo @echo "Integration Tests:" @echo $(GO_INTEGRATION_TESTS) @echo @echo "Core Tests:" @echo $(GO_CORE_TESTS) @echo go-agent-3.42.0/README.md000066400000000000000000000650151510742411500145670ustar00rootroot00000000000000[![Community Plus header](https://github.com/newrelic/opensource-website/raw/main/src/images/categories/Community_Plus.png)](https://opensource.newrelic.com/oss-category/#community-plus) # New Relic Go Agent [![GoDoc](https://godoc.org/github.com/newrelic/go-agent?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) [![Go Report Card](https://goreportcard.com/badge/github.com/newrelic/go-agent)](https://goreportcard.com/report/github.com/newrelic/go-agent) [![codecov](https://codecov.io/github/newrelic/go-agent/branch/master/graph/badge.svg?token=UEWy0clWYW)](https://codecov.io/github/newrelic/go-agent) The New Relic Go Agent allows you to monitor your Go applications with New Relic. It helps you track transactions, outbound requests, database calls, and other parts of your Go application's behavior and provides a running overview of garbage collection, goroutine activity, and memory use. Go is a compiled language, and doesn’t use a virtual machine. This means that setting up New Relic for your Golang app requires you to use our Go agent API and manually add New Relic methods to your source code. Our API provides exceptional flexibility and control over what gets instrumented. ## Installation ### Compatibility and Requirements For the latest version of the agent, Go 1.22+ is required. Linux, OS X, and Windows (Vista, Server 2008 and later) are supported. ### Installing and using the Go agent To install the agent, follow the instructions in our [GETTING_STARTED](https://github.com/newrelic/go-agent/blob/master/GETTING_STARTED.md) document or our [GUIDE](https://github.com/newrelic/go-agent/blob/master/GUIDE.md). We recommend instrumenting your Go code to get the maximum benefits from the New Relic Go agent. But we make it easy to get great data in couple of ways: * Even without adding instrumentation, just importing the agent and creating an application will provide useful runtime information about your number of goroutines, garbage collection statistics, and memory and CPU usage. * You can use our many [INTEGRATION packages](https://github.com/newrelic/go-agent/tree/master/v3/integrations) for out-of-the box support for many popular Go web frameworks and libraries. We continue to add integration packages based on your feedback. You can weigh in on potential integrations by opening an `Issue` here in our New Relic Go agent GitHub project. ### Upgrading If you have already been using version 2.X of the agent and are upgrading to version 3.0, see our [MIGRATION guide](MIGRATION.md) for details. ## Getting Started [v3/examples/server/main.go](v3/examples/server/main.go) is an example that will appear as "Example App" in your New Relic applications list. To run it: ``` env NEW_RELIC_LICENSE_KEY=__YOUR_NEW_RELIC_LICENSE_KEY__LICENSE__ \ go run v3/examples/server/main.go ``` Some endpoints exposed are [http://localhost:8000/](http://localhost:8000/) and [http://localhost:8000/notice_error](http://localhost:8000/notice_error) ## Usage ### Integration Packages The following [integration packages](https://godoc.org/github.com/newrelic/go-agent/v3/integrations) extend the base [newrelic](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) package to support the following frameworks and libraries. Frameworks and databases which don't have an integration package may still be instrumented using the [newrelic](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) package primitives. #### Service Frameworks | Project | Integration Package | | | ------------- | ------------- | - | | [gin-gonic/gin](https://github.com/gin-gonic/gin) | [v3/integrations/nrgin](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin) | Instrument inbound requests through the Gin framework | | [gofiber/fiber](https://github.com/gofiber/fiber) | [v3/integrations/nrfiber](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrfiber) | Instrument inbound requests through the Fiber framework | | [gorilla/mux](https://github.com/gorilla/mux) | [v3/integrations/nrgorilla](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla) | Instrument inbound requests through the Gorilla framework | | [google.golang.org/grpc](https://github.com/grpc/grpc-go) | [v3/integrations/nrgrpc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgrpc) | Instrument gRPC servers and clients | | [labstack/echo](https://github.com/labstack/echo) | [v3/integrations/nrecho-v3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3) | Instrument inbound requests through version 3 of the Echo framework | | [labstack/echo](https://github.com/labstack/echo) | [v3/integrations/nrecho-v4](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4) | Instrument inbound requests through version 4 of the Echo framework | | [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter) | [v3/integrations/nrhttprouter](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrhttprouter) | Instrument inbound requests through the HttpRouter framework | | [micro/go-micro](https://github.com/micro/go-micro) | [v3/integrations/nrmicro](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmicro) | Instrument servers, clients, publishers, and subscribers through the Micro framework | | [connectrpc/connect-go](https://github.com/connectrpc/connect-go) | [v3/integrations/nrconnect](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrconnect) | Instrument Connect servers and clients | #### Datastores More information about instrumenting databases without an integration package using [newrelic](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) package primitives can be found [here](GUIDE.md#datastore-segments). | Project | Integration Package | | | ------------- | ------------- | - | | [lib/pq](https://github.com/lib/pq) | [v3/integrations/nrpq](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq) | Instrument PostgreSQL driver (`pq` driver for `database/sql`) | | [jackc/pgx](https://github.com/jackc/pgx) | [v3/integrations/nrpgx](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx) | Instrument PostgreSQL driver (`pgx` driver for `database/sql`)| | [jackc/pgx/v5](https://github.com/jackc/pgx/v5) | [v3/integrations/nrpgx5](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5) | Instrument PostgreSQL driver (`pgx/v5` driver for `database/sql`)| | [go-mssqldb](github.com/denisenkom/go-mssqldb) | [v3/integrations/nrmssql](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmssql) | Instrument MS SQL driver | | [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) | [v3/integrations/nrmysql](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql) | Instrument MySQL driver | | [elastic/go-elasticsearch](https://github.com/elastic/go-elasticsearch) | [v3/integrations/nrelasticsearch-v7](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7) | Instrument Elasticsearch datastore calls | | [database/sql](https://godoc.org/database/sql) | Use a supported database driver or [builtin instrumentation](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#InstrumentSQLConnector) | Instrument database calls with SQL | | [jmoiron/sqlx](https://github.com/jmoiron/sqlx) | Use a supported [database driver](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq/example/sqlx) or [builtin instrumentation](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#InstrumentSQLConnector) | Instrument database calls with SQLx | | [go-redis/redis](https://github.com/go-redis/redis) | [v3/integrations/nrredis-v7](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7) | Instrument Redis 7 calls | | [go-redis/redis](https://github.com/go-redis/redis) | [v3/integrations/nrredis-v8](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8) | Instrument Redis 8 calls | | [redis/go-redis](https://github.com/redis/go-redis) | [v3/integrations/nrredis-v9](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v9) | Instrument Redis 9 calls | | [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) | [v3/integrations/nrsqlite3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsqlite3) | Instrument SQLite driver | | [snowflakedb/gosnowflake](https://github.com/snowflakedb/gosnowflake) | [v3/integrations/nrsnowflake](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsnowflake) | Instrument Snowflake driver | | [mongodb/mongo-go-driver](https://github.com/mongodb/mongo-go-driver) | [v3/integrations/nrmongo](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo) | Instrument MongoDB calls | #### AI | Project | Integration Package | | | ------------- | ------------- | - | | [sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) | [v3/integrations/nropenai](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nropenai) | Send AI Monitoring Events with OpenAI | | [aws/aws-sdk-go-v2/tree/main/service/bedrockruntime](https://github.com/aws/aws-sdk-go-v2/tree/main/service/bedrockruntime) | [v3/integrations/nrawsbedrock](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawsbedrock) | Send AI Monitoring Events with AWS Bedrock | #### Agent Logging | Project | Integration Package | | |-------------------------------------------------------|-----------------------------------------------------------------------------------------------------|---------------------------------------| | [sirupsen/logrus](https://github.com/sirupsen/logrus) | [v3/integrations/nrlogrus](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus) | Send agent log messages to Logrus | | [mgutz/logxi](https://github.com/mgutz/logxi) | [v3/integrations/nrlogxi](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi) | Send agent log messages to Logxi | | [uber-go/zap](https://github.com/uber-go/zap) | [v3/integrations/nrzap](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap) | Send agent log messages to Zap | | [log/slog](https://pkg.go.dev/log/slog) | [v3/integrations/nrslog](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrslog) | Send agent log messages to `log/slog` | | [rs/zerolog](https://github.com/rs/zerolog) | [v3/integrations/nrzerolog](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog) | Send agent log messages to Zerolog | #### Logs in Context | Project | Integration Package | | | ------------- | ------------- | - | | [sirupsen/logrus](https://github.com/sirupsen/logrus) | [v3/integrations/logcontext-v2/nrlogrus](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus) | Send data collected from Logrus log messages to New Relic | | [log](https://pkg.go.dev/log) | [v3/integrations/logcontext-v2/logWriter](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext-v2/logWriter) | Send data collected from the standard library logger log messages to New Relic | | [rs/zerolog](https://github.com/rs/zerolog) | [v3/integrations/logcontext-v2/zerologWriter](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter) | Send data collected from zerolog log messages to New Relic | #### AWS | Project | Integration Package | | | ------------- | ------------- | - | | [aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) | [v3/integrations/nrawssdk-v1](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1) | Instrument outbound calls made using Go AWS SDK | | [aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | [v3/integrations/nrawssdk-v2](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2) | Instrument outbound calls made using Go AWS SDK v2 | | [aws/aws-lambda-go](https://github.com/aws/aws-lambda-go) | [v3/integrations/nrlambda](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlambda) | Instrument AWS Lambda applications | #### GraphQL | Project | Integration Package | | | ------------- | ------------- | - | | [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go) | [v3/integrations/nrgraphgophers](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers) | Instrument inbound requests using graph-gophers/graphql-go | | [graphql-go/graphql](https://github.com/graphql-go/graphql) | [v3/integrations/nrgraphqlgo](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo) | Instrument inbound requests using graphql-go/graphql | #### Misc | Project | Integration Package | | | ------------- | ------------- | - | | [pkg/errors](https://github.com/pkg/errors) | [v3/integrations/nrpkgerrors](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpkgerrors) | Wrap pkg/errors errors to improve stack traces and error class information | | [openzipkin/b3-propagation](https://github.com/openzipkin/b3-propagation) | [v3/integrations/nrb3](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrb3) | Add B3 headers to outgoing requests | | [nats-io/nats.go](https://github.com/nats-io/nats.go) | [v3/integrations/nrnats](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats) | Instrument publishers and subscribers using the NATS client | | [nats-io/stan.go](https://github.com/nats-io/stan.go) | [v3/integrations/nrstan](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrstan) | Instrument publishers and subscribers using the NATS streaming client | These integration packages must be imported along with the [newrelic](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) package, as shown in this [nrgin example](https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgin/example/main.go). ### Alternatives If you are already using another open source solution to gather telemetry data, you may find it easier to use one of our open source exporters to send this data to New Relic: * OpenTelemetry: [github.com/newrelic/opentelemetry-exporter-go](https://github.com/newrelic/opentelemetry-exporter-go) * OpenCensus: [github.com/newrelic/newrelic-opencensus-exporter-go](https://github.com/newrelic/newrelic-opencensus-exporter-go) * Prometheus Exporter: [github.com/newrelic/nri-prometheus](https://github.com/newrelic/nri-prometheus) * Istio Adapter: [github.com/newrelic/newrelic-istio-adapter](https://github.com/newrelic/newrelic-istio-adapter) * Telemetry SDK: [github.com/newrelic/newrelic-telemetry-sdk-go](https://github.com/newrelic/newrelic-telemetry-sdk-go) ## Go Agent Development This section describes the suggested workflow for developers contributing to the Go Agent. ### The Makefile At the root directory is a Makefile that contains several helpful targets for building containers and running tests. ### Docker [Docker](https://www.docker.com/) is a useful development tool that allows for the quick, easy and repeatable creation of virtual environments. [Docker Compose](https://docs.docker.com/compose/) builds on the functionality provided by Docker to control the orchestration of multi-container environments. The development workflow described in this section relies heavily Docker. The containers that are generated will create a [volume](https://docs.docker.com/engine/storage/volumes/) mounted to the current directory on the local host, allowing the modification of code and tests on the local host to be propagated automatically to the running container without having to re-build or copy manually. ### Targets | Target | Description | |--------|-------------| | `devenv-image` | Build the dev image(s) specified under the `docker compose` `dev` profile. | | `dev-shell` | Run `docker compose` with the `dev` profile. Open a shell in the running container. | | `dev-stop` | Terminate the running containers under the `docker compose` `dev` profile. | | `test-services-start` | Spin up agent and other containers. | | `test-services-stop` | Terminate agent and other containers. | | `core-test` | Run a specified core test. | | `core-suite` | Run all core tests. | | `integration-test` | Run a specified integration test. | | `integration-suite` | Run all integration tests. | | `tidy` | Replace github.com New Relic Go Agent Module with the local copy; run `go mod tidy`. | | `info` | Display information about the running Go environment, integration tests, and core tests. | ### Usage Build and run the agent container and database container(s). Execute a shell into the agent container environment: ``` make dev-shell ``` Terminate the container(s) started by `dev-shell`: ``` make dev-stop ``` Spin up services for testing. Does not create a shell into the container. May specify an optional `PROFILE` argument to start another container not covered by the default `test` profile: *Note*: When running the `integration-suite` against `test-services` containers, all accessory `PROFILE`s must be specified or the tests will fail. This may require editing the docker-compose.yml file. It is recommended to instead use `dev-shell` for this kind of use case, as the `dev` profile will start every integration container by default. ``` make test-services-start ``` Terminate the containers started by the `test-services-start` target: ``` make test-services-stop ``` Run a core test. May specify optional `COVERAGE` argument to generate a coverprofile: ``` make core-test TEST=test-name ``` Run all core tests: ``` make core-suite ``` Run an integration test. May specify optional `COVERAGE` argument to generate a coverprofile: ``` make integration-test TEST=test-name ``` Run all integration tests: ``` make integration-suite ``` ### Example (dev-shell) An example development workflow where the developer is testing changes to `utilization` functionality and the `nrpgx5` integration might look like the following: From the top-level checkout of the go-agent, start the containers and open a shell into the agent container: ``` local:go-agent user$ make dev-shell [+] Building 0/0 ...Docker build output... [+] Building 1/1 docker.io/library/go-agent-devenv 0.0s ✔ Service devenv Built 0.8s docker compose --profile dev up --pull missing --remove-orphans -d [+] Running 2/2 ✔ Container go-agent-postgres-1 Started 0.2s ✔ Container go-agent-devenv-1 Started 0.2s docker compose exec -it devenv bash -c "bash" root@5a832b6fcba3:/usr/src/app/go-agent# ``` Testing the `utilization` package: ``` root@5a832b6fcba3:/usr/src/app/go-agent# make core-test TEST=internal/utilization # TEST=internal/utilization, COVERAGE= go: downloading packages... PASS ok github.com/newrelic/go-agent/v3/internal/utilization 0.116s # TEST=internal/utilization root@5a832b6fcba3:/usr/src/app/go-agent# ``` Testing the `nrpgx5` integration: ``` root@5a832b6fcba3:/usr/src/app/go-agent# make integration-test TEST=nrpgx5 # TEST=nrpgx5, COVERAGE= go: downloading packages... PASS ok github.com/newrelic/go-agent/v3/integrations/nrpgx5 1.100s # TEST=nrpgx5 root@5a832b6fcba3:/usr/src/app/go-agent# ``` Make the necessary code changes, and test using the steps above until satisfied. Once done with development, terminate the running containers: ``` root@5a832b6fcba3:/usr/src/app/go-agent# exit exit local:go-agent user$ make dev-stop docker compose --profile dev stop [+] Stopping 2/2 ✔ Container go-agent-devenv-1 Stopped 10.1s ✔ Container go-agent-postgres-1 Stopped 0.1s local:go-agent user$ ``` ### Example (test-services) The `test-services-*` targets are designed to allow developers to run tests without needing to execute a shell into the agent container. Re-using the scenario described above in `dev-shell`, this workflow might look like the following: From the top-level checkout of the go-agent: ``` local:go-agent user$ make test-services-start PROFILE=nrpgx5 [+] Pulling 16/16 ✔ go Skipped - No image to be pulled 0.0s ✔ postgres Pulled 7.0s ...Docker build output... ✔ Service go Built 0.9s ✔ Container nr-go Healthy 1.1s ✔ Container go-agent-postgres-1 Healthy 1.1s local:go-agent user$ ``` *Note*: the specification of `PROFILE=nrpgx5`. This ensures that the postgres container is started alongside the agent container in order to test the `nrpgx5` integration. This is not necessary if you do not intend to run postgres tests. Testing the `utilization` package: ``` local:go-agent user$ docker exec -e TEST=internal/utilization nr-go make core-test # TEST=internal/utilization, COVERAGE= go: downloading packages... PASS ok github.com/newrelic/go-agent/v3/internal/utilization 0.105s # TEST=internal/utilization local:go-agent user$ ``` Let's quickly break down the above docker command: 1. [docker exec](https://docs.docker.com/reference/cli/docker/container/exec/) executes a command in a running container. 2. `-e TEST=internal/utilization` passes the `TEST` argument as an environment variable. 3. `nr-go` is the name of the container where the command will be executed. 4. `make core-test` is the command to be executed. The same logic applies to running integration tests: ``` local:go-agent user$ docker exec -e TEST=nrpgx5 nr-go make integration-test # TEST=nrpgx5, COVERAGE= go: downloading packages... PASS ok github.com/newrelic/go-agent/v3/integrations/nrpgx5 1.095s # TEST=nrpgx5 local:go-agent user$ ``` Like the `dev-shell` target, the `test-services` agent container is volume-mounted to the local working directory containing the agent checkout. Code and test changes can be made and will automatically propagate to the container environment. To terminate the running containers: ``` local:go-agent user$ make test-services-stop PROFILE=nrpgx5 [+] Stopping 2/2 ✔ Container go-agent-postgres-1 Stopped 0.1s ✔ Container nr-go Stopped 10.1s local:go-agent user$ ``` ### Adding new tests or moving existing tests When adding or moving tests, any changes must be reflected in the corresponding `integration-tests.mk` / `core-tests.mk` files. `core-tests.mk` contains all tests under the `v3/newrelic` and `v3/internal` directories and subdirectories. `integration-tests.mk` contains all tests under the `v3/integrations` directory and subdirectories. ### Development with different Go Versions Docker Compose uses the latest Go Version for the agent container by default. At the time of writing, this is `1.24`. This value can be changed by exporting a `GO_VERSION` environment variable or top-level `.env` file containing the `GO_VERSION` definition. Example using Go 1.23: ``` export GO_VERSION=1.23 ``` Then start docker services as normal. ## Support Should you need assistance with New Relic products, you are in good hands with several support channels. If the issue has been confirmed as a bug or is a Feature request, please file a Github issue. * [Go Agent GUIDE](GUIDE.md): Step by step how-to for key agent features * [New Relic Documentation](https://docs.newrelic.com/docs/agents/go-agent): Comprehensive guidance for using our platform * [Troubleshooting framework](https://discuss.newrelic.com/t/troubleshooting-frameworks/108787): Steps you through common troubleshooting questions * [New Relic Community](https://discuss.newrelic.com/tags/goagent): The best place to engage in troubleshooting questions * [New Relic Developer](https://developer.newrelic.com/): Resources for building a custom observability applications * [New Relic University](https://learn.newrelic.com/): A range of online training for New Relic users of every level ## Privacy 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. 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. For more information, review [New Relic’s General Data Privacy Notice](https://newrelic.com/termsandconditions/privacy). ## Contribute We encourage your contributions to improve the Go Agent! 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. If you have any questions, or to execute our corporate CLA, required if your contribution is on behalf of a company, please drop us an email at opensource@newrelic.com. **A note about vulnerabilities** As noted in our [security policy](https://github.com/newrelic/go-agent/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. 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). If you would like to contribute to this project, please review [these guidelines](./CONTRIBUTING.md). To [all contributors](https://github.com/newrelic/go-agent/graphs/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 the [Go Agent](https://opensource.newrelic.com/projects/newrelic/go-agent). ## License The New Relic Go agent is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License. go-agent-3.42.0/THIRD_PARTY_NOTICES.md000066400000000000000000000042671510742411500166510ustar00rootroot00000000000000# Third Party Notices The New Relic Go Agent uses source code from third party libraries which carry their own copyright notices and license terms. These notices are provided below. In the event that a required notice is missing or incorrect, please notify us either by [opening an issue](https://github.com/newrelic/go-agent/issues/new), or by e-mailing [open-source@newrelic.com](mailto:open-source@newrelic.com). For any licenses that require the disclosure of source code, the source code can be found at https://github.com/newrelic/go-agent/. ## Contents * [Go](#go) ## Go This product includes source derived from the [Go programming language](https://golang.org/), distributed under the [Go license](https://golang.org/LICENSE): ``` Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` go-agent-3.42.0/codecov.yml000066400000000000000000000006641510742411500154540ustar00rootroot00000000000000coverage: status: project: true patch: true changes: false ignore: - "go-agent/v3/internal/com_newrelic_trace_v1" - "go-agent/v3/internal/crossagent" - "go-agent/v3/internal/logcontext" - "go-agent/v3/internal/stacktracetest" - "go-agent/v3/internal/tools" - "go-agent/v3/newrelic/sql_driver_optional_methods.go" comment: layout: "reach,diff,flags,files,footer" behavior: default require_changes: falsego-agent-3.42.0/core-tests.mk000066400000000000000000000007501510742411500157240ustar00rootroot00000000000000# # Copyright 2025 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Core Tests on 3 most recent major Go versions GO_CORE_TESTS=$${CORE_TESTS:-\ newrelic \ newrelic/integrationsupport \ newrelic/sqlparse \ internal \ internal/awssupport \ internal/cat \ internal/com_newrelic_trace_v1 \ internal/crossagent \ internal/jsonx \ internal/logcontext \ internal/logger \ internal/stacktracetest \ internal/sysinfo \ internal/utilization \ } go-agent-3.42.0/docker-compose.yml000066400000000000000000000035061510742411500167420ustar00rootroot00000000000000# # Copyright 2025 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # x-mongo-env: &mongo-env MONGO_INITDB_ROOT_USERNAME: admin MONGO_INITDB_ROOT_PASSWORD: password MONGO_HOST: mongodb MONGO_PORT: 27017 MONGO_DB: test services: postgres: image: postgres restart: always environment: POSTGRES_PASSWORD: root POSTGRES_USER: postgres profiles: ["dev", "nrpgx5"] mongodb: image: mongo:latest restart: always environment: <<: *mongo-env ports: - "27017:27017" healthcheck: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet interval: 10s timeout: 10s retries: 3 start_period: 20s container_name: mongodb-1 profiles: ["dev", "nrmongo", "nrmongo-v2"] #profiles: ["mongo"] - for local testing go: build: context: . dockerfile: ./Dockerfile args: GO_VERSION: ${GO_VERSION:-1.25} environment: PG_HOST: postgres PG_PORT: 5432 PG_USER: postgres PG_PW: root PG_DB: postgres PG_PARAM: "?connect_timeout=10&sslmode=disable" <<: *mongo-env volumes: - ${AGENT_CODE:-$PWD}:/usr/src/app/go-agent working_dir: /usr/src/app/go-agent entrypoint: tail command: -f /dev/null container_name: nr-go profiles: ["test", "nrpgx5"] devenv: build: context: . dockerfile: ./Dockerfile args: GO_VERSION: ${GO_VERSION:-1.25} environment: PG_HOST: postgres PG_PORT: 5432 PG_USER: postgres PG_PW: root PG_DB: postgres PG_PARAM: "?connect_timeout=10&sslmode=disable" <<: *mongo-env volumes: - ${PWD}:/usr/src/app/go-agent working_dir: /usr/src/app/go-agent stdin_open: true tty: true profiles: ["dev"] go-agent-3.42.0/integration-tests.mk000066400000000000000000000017011510742411500173140ustar00rootroot00000000000000# # Copyright 2025 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Integration tests for the highest supported Go Version GO_INTEGRATION_TESTS=$${INTEGRATION_TESTS:-\ logcontext/nrlogrusplugin \ logcontext-v2/logWriter \ logcontext-v2/nrlogrus \ logcontext-v2/nrslog \ logcontext-v2/nrwriter \ logcontext-v2/nrzap \ logcontext-v2/nrzerolog \ logcontext-v2/zerologWriter \ nramqp \ nrawsbedrock \ nrawssdk-v1 \ nrawssdk-v2 \ nrb3 \ nrconnect \ nrecho-v3 \ nrecho-v4 \ nrelasticsearch-v7 \ nrfasthttp \ nrfiber \ nrgin \ nrgochi \ nrgorilla \ nrgraphgophers \ nrgraphqlgo \ nrgrpc \ nrhttprouter \ nrlambda \ nrlogrus \ nrlogxi \ nrmicro \ nrmongo \ nrmongo-v2 \ nrmssql \ nrmysql \ nrnats \ nropenai \ nrpgx \ nrpgx5 \ nrpkgerrors \ nrpq \ nrredis-v7 \ nrredis-v8 \ nrredis-v9 \ nrsarama \ nrsecurityagent \ nrslog \ nrsnowflake \ nrsqlite3 \ nrzap \ nrzerolog \ } go-agent-3.42.0/run-tests.sh000077500000000000000000000011371510742411500156060ustar00rootroot00000000000000#!/bin/bash # run_tests.sh export PATH=$PATH:/usr/local/go/bin # Test directory is passed in as an argument TEST_DIR=$1 # Function for checking Go Code Formatting verify_go_fmt() { needsFMT=$(gofmt -d .) if [ ! -z "$needsFMT" ]; then echo "$needsFMT" echo "Please format your code with \"gofmt .\"" # exit 1 fi } # Replace go-agent with local pull cd go-agent/v3 go mod edit -replace github.com/newrelic/go-agent/v3="$(pwd)/v3" cd ../ cd $TEST_DIR go mod tidy # Run Tests and Create Cover Profile for Code Coverage go test -race -benchtime=1ms -bench=. ./... go vet ./... verify_go_fmtgo-agent-3.42.0/v3/000077500000000000000000000000001510742411500136315ustar00rootroot00000000000000go-agent-3.42.0/v3/LICENSE.txt000066400000000000000000000264501510742411500154630ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/examples/000077500000000000000000000000001510742411500154475ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/client/000077500000000000000000000000001510742411500167255ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/client/main.go000066400000000000000000000021161510742411500202000ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "net/http" "os" "time" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func doRequest(txn *newrelic.Transaction) error { req, err := http.NewRequest("GET", "http://localhost:8000/segments", nil) if err != nil { return err } client := &http.Client{} seg := newrelic.StartExternalSegment(txn, req) defer seg.End() resp, err := client.Do(req) if err != nil { return err } fmt.Println("response code is", resp.StatusCode) return nil } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Client App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if err != nil { fmt.Println(err) os.Exit(1) } txn := app.StartTransaction("client-txn") err = doRequest(txn) if err != nil { txn.NoticeError(err) } txn.End() // Shut down the application to flush data to New Relic. app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/examples/custom-instrumentation/000077500000000000000000000000001510742411500222225ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/custom-instrumentation/main.go000066400000000000000000000053461510742411500235050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // An application that illustrates Distributed Tracing with custom // instrumentation. // // This application simulates simple inter-process communication between a // calling and a called process. // // Invoked without arguments, the application acts as a calling process; // invoked with one argument representing a payload, it acts as a called // process. The calling process creates a payload, starts a called process and // passes on the payload. The calling process waits until the called process is // done and then terminates. Thus to start both processes, only a single // invocation of the application (without any arguments) is needed. package main import ( "fmt" "net/http" "os" "os/exec" "time" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func called(app *newrelic.Application, payload string) { txn := app.StartTransaction("called-txn") defer txn.End() // Accept the payload that was passed on the command line. hdrs := http.Header{} hdrs.Set(newrelic.DistributedTraceNewRelicHeader, payload) txn.AcceptDistributedTraceHeaders(newrelic.TransportOther, hdrs) time.Sleep(1 * time.Second) } func calling(app *newrelic.Application) { txn := app.StartTransaction("calling-txn") defer txn.End() // Create a payload, start the called process and pass the payload. hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) cmd := exec.Command(os.Args[0], hdrs.Get(newrelic.DistributedTraceNewRelicHeader)) cmd.Start() // Wait until the called process is done, then exit. cmd.Wait() time.Sleep(1 * time.Second) } func makeApplication(name string) (*newrelic.Application, error) { app, err := newrelic.NewApplication( newrelic.ConfigAppName(name), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if nil != err { return nil, err } // Wait for the application to connect. if err = app.WaitForConnection(5 * time.Second); nil != err { return nil, err } return app, nil } func main() { // Calling processes have no command line arguments, called processes // have one command line argument (the payload). isCalled := (len(os.Args) > 1) // Initialize the application name. name := "Go Custom Instrumentation" if isCalled { name += " Called" } else { name += " Calling" } // Initialize the application. app, err := makeApplication(name) if nil != err { fmt.Println(err) os.Exit(1) } // Run calling/called routines. if isCalled { payload := os.Args[1] called(app, payload) } else { calling(app) } // Shut down the application to flush data to New Relic. app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/examples/oom/000077500000000000000000000000001510742411500162415ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/oom/main.go000066400000000000000000000027031510742411500175160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "runtime" "time" "github.com/newrelic/go-agent/v3/newrelic" ) const MB = 1024 * 1024 func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("OOM Response High Water Mark App"), newrelic.ConfigFromEnvironment(), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { fmt.Println(err) os.Exit(1) } // Wait for the application to connect. if err := app.WaitForConnection(5 * time.Second); err != nil { fmt.Println(err) } app.HeapHighWaterMarkAlarmSet(1*MB, megabyte) app.HeapHighWaterMarkAlarmSet(10*MB, tenMegabyte) app.HeapHighWaterMarkAlarmSet(100*MB, hundredMegabyte) app.HeapHighWaterMarkAlarmEnable(2 * time.Second) var a [][]byte for _ = range 100 { a = append(a, make([]byte, MB, MB)) time.Sleep(1 * time.Second) } // Shut down the application to flush data to New Relic. app.Shutdown(10 * time.Second) } func megabyte(limit uint64, stats *runtime.MemStats) { fmt.Printf("*** 1M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) } func tenMegabyte(limit uint64, stats *runtime.MemStats) { fmt.Printf("*** 10M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) } func hundredMegabyte(limit uint64, stats *runtime.MemStats) { fmt.Printf("*** 100M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) } go-agent-3.42.0/v3/examples/server-http/000077500000000000000000000000001510742411500177325ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/server-http/main.go000066400000000000000000000033151510742411500212070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // An application that illustrates Distributed Tracing or Cross Application // Tracing when using http.Server or similar frameworks. package main import ( "fmt" "io" "net/http" "os" "time" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) type handler struct { App *newrelic.Application } func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { // The call to StartTransaction must include the response writer and the // request. txn := h.App.StartTransaction("server-txn") defer txn.End() writer = txn.SetWebResponse(writer) txn.SetWebRequestHTTP(req) if req.URL.String() == "/segments" { defer txn.StartSegment("f1").End() func() { defer txn.StartSegment("f2").End() io.WriteString(writer, "segments!") time.Sleep(10 * time.Millisecond) }() time.Sleep(10 * time.Millisecond) } else { // Transaction.WriteHeader has to be used instead of invoking // WriteHeader on the response writer. writer.WriteHeader(http.StatusNotFound) } } func makeApplication() (*newrelic.Application, error) { app, err := newrelic.NewApplication( newrelic.ConfigAppName("HTTP Server App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { return nil, err } // Wait for the application to connect. if err = app.WaitForConnection(5 * time.Second); nil != err { return nil, err } return app, nil } func main() { app, err := makeApplication() if nil != err { fmt.Println(err) os.Exit(1) } server := http.Server{ Addr: ":8000", Handler: &handler{App: app}, } server.ListenAndServe() } go-agent-3.42.0/v3/examples/server/000077500000000000000000000000001510742411500167555ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/server/Dockerfile000066400000000000000000000024561510742411500207560ustar00rootroot00000000000000# If it is more convenient for you to run an instrumented test server in a Docker # container, you can use this Dockerfile to build an image for that purpose. # # To build this image, have this Dockerfile in the current directory and run: # docker build -t go-agent-test . # # To run a test, run the following: # docker run -e NEW_RELIC_LICENSE_KEY="YOUR_KEY_HERE" -p 127.0.0.1:8000:8000 go-agent-test # then drive traffic to it on localhost port 8000 # # This running application will write debugging logs showing all interaction # with the collector on its standard output. # # The following HTTP endpoints can be accessed on port 8000 to invoke different # instrumented server features: # / # /add_attribute # /add_span_attribute # /async # /background # /background_log # /browser # /custom_event # /custommetric # /external # /ignore # /log # /message # /mysql # /notice_error # /notice_error_with_attributes # /notice_expected_error # /roundtripper # /segments # /set_name # /version # FROM golang:1.25 MAINTAINER Steve Willoughby WORKDIR /go RUN git clone https://github.com/newrelic/go-agent WORKDIR /go/go-agent/v3 RUN go mod tidy WORKDIR /go/go-agent/v3/examples/server RUN go mod tidy RUN go build EXPOSE 8000 CMD ["/go/go-agent/v3/examples/server/server"] # # END # go-agent-3.42.0/v3/examples/server/main.go000066400000000000000000000234711510742411500202370ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "io" "math/rand" "net/http" "os" "sync" "time" "github.com/newrelic/go-agent/v3/newrelic" ) func index(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello world") } func versionHandler(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "New Relic Go Agent Version: "+newrelic.Version) } func noticeError(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "noticing an error") txn := newrelic.FromContext(r.Context()) txn.NoticeError(errors.New("my error message")) } func noticeExpectedError(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "noticing an error") txn := newrelic.FromContext(r.Context()) txn.NoticeExpectedError(errors.New("my expected error message")) } func noticeErrorWithAttributes(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "noticing an error") txn := newrelic.FromContext(r.Context()) txn.NoticeError(newrelic.Error{ Message: "uh oh. something went very wrong", Class: "errors are aggregated by class", Attributes: map[string]interface{}{ "important_number": 97232, "relevant_string": "zap", }, }) } func customEvent(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) io.WriteString(w, "recording a custom event") txn.Application().RecordCustomEvent("my_event_type", map[string]interface{}{ "myString": "hello", "myFloat": 0.603, "myInt": 123, "myBool": true, }) } func setName(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "changing the transaction's name") txn := newrelic.FromContext(r.Context()) txn.SetName("other-name") } func addAttribute(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "adding attributes") txn := newrelic.FromContext(r.Context()) txn.AddAttribute("myString", "hello") txn.AddAttribute("myInt", 123) } func addSpanAttribute(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "adding span attributes") txn := newrelic.FromContext(r.Context()) sgmt := txn.StartSegment("segment1") defer sgmt.End() sgmt.AddAttribute("mySpanString", "hello") sgmt.AddAttribute("mySpanInt", 123) } func ignore(w http.ResponseWriter, r *http.Request) { if coinFlip := (0 == rand.Intn(2)); coinFlip { txn := newrelic.FromContext(r.Context()) txn.Ignore() io.WriteString(w, "ignoring the transaction") } else { io.WriteString(w, "not ignoring the transaction") } } func segments(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) func() { defer txn.StartSegment("f1").End() func() { defer txn.StartSegment("f2").End() io.WriteString(w, "segments!") time.Sleep(10 * time.Millisecond) }() time.Sleep(15 * time.Millisecond) }() time.Sleep(20 * time.Millisecond) } func mysql(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) s := newrelic.DatastoreSegment{ StartTime: txn.StartSegmentNow(), // Product, Collection, and Operation are the most important // fields to populate because they are used in the breakdown // metrics. Product: newrelic.DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: map[string]interface{}{ "name": "Dracula", "age": 439, }, Host: "mysql-server-1", PortPathOrID: "3306", DatabaseName: "my_database", } defer s.End() time.Sleep(20 * time.Millisecond) io.WriteString(w, `performing fake query "INSERT * from users"`) } func message(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) s := newrelic.MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: newrelic.MessageQueue, DestinationName: "myQueue", } defer s.End() time.Sleep(20 * time.Millisecond) io.WriteString(w, `producing a message queue message`) } func external(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) req, _ := http.NewRequest("GET", "https://example.com", nil) // Using StartExternalSegment is recommended because it does distributed // tracing header setup, but if you don't have an *http.Request and // instead only have a url string then you can start the external // segment like this: // // es := newrelic.ExternalSegment{ // StartTime: txn.StartSegmentNow(), // URL: urlString, // } // es := newrelic.StartExternalSegment(txn, req) resp, err := http.DefaultClient.Do(req) es.End() if err != nil { io.WriteString(w, err.Error()) return } defer resp.Body.Close() io.Copy(w, resp.Body) } func roundtripper(w http.ResponseWriter, r *http.Request) { // NewRoundTripper allows you to instrument external calls without // calling StartExternalSegment by modifying the http.Client's Transport // field. If the Transaction parameter is nil, the RoundTripper // returned will look for a Transaction in the request's context (using // FromContext). This is recommended because it allows you to reuse the // same client for multiple transactions. client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) request, _ := http.NewRequest("GET", "https://example.com", nil) // Since the transaction is already added to the inbound request's // context by WrapHandleFunc, we just need to copy the context from the // inbound request to the external request. request = request.WithContext(r.Context()) // Alternatively, if you don't want to copy entire context, and instead // wanted just to add the transaction to the external request's context, // you could do that like this: // // txn := newrelic.FromContext(r.Context()) // request = newrelic.RequestWithTransactionContext(request, txn) resp, err := client.Do(request) if err != nil { io.WriteString(w, err.Error()) return } defer resp.Body.Close() io.Copy(w, resp.Body) } func async(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) wg := &sync.WaitGroup{} wg.Add(1) go func(txn *newrelic.Transaction) { defer wg.Done() defer txn.StartSegment("async").End() time.Sleep(100 * time.Millisecond) }(txn.NewGoroutine()) segment := txn.StartSegment("wg.Wait") wg.Wait() segment.End() w.Write([]byte("done!")) } func customMetric(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) for _, vals := range r.Header { for _, v := range vals { // This custom metric will have the name // "Custom/HeaderLength" in the New Relic UI. txn.Application().RecordCustomMetric("HeaderLength", float64(len(v))) } } io.WriteString(w, "custom metric recorded") } func browser(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) hdr := txn.BrowserTimingHeader() // BrowserTimingHeader() will always return a header whose methods can // be safely called. if js := hdr.WithTags(); js != nil { w.Write(js) } io.WriteString(w, "browser header page") } func logTxnMessage(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) txn.RecordLog(newrelic.LogData{ Message: "Log Message", Severity: "info", }) io.WriteString(w, "A log message was recorded") } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigFromEnvironment(), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), newrelic.ConfigCodeLevelMetricsEnabled(true), newrelic.ConfigCodeLevelMetricsPathPrefix("go-agent/v3"), ) if err != nil { fmt.Println(err) os.Exit(1) } http.HandleFunc(newrelic.WrapHandleFunc(app, "/", index)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/version", versionHandler)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_error", noticeError)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_expected_error", noticeExpectedError)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_error_with_attributes", noticeErrorWithAttributes)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/custom_event", customEvent)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/set_name", setName)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/add_attribute", addAttribute)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/add_span_attribute", addSpanAttribute)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/ignore", ignore)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/segments", segments)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/mysql", mysql)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/external", external)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/roundtripper", roundtripper)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/custommetric", customMetric)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/browser", browser)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/async", async)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/message", message)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/log", logTxnMessage)) //loc := newrelic.ThisCodeLocation() backgroundCache := newrelic.NewCachedCodeLocation() http.HandleFunc("/background", func(w http.ResponseWriter, req *http.Request) { // Transactions started without an http.Request are classified as // background transactions. txn := app.StartTransaction("background", backgroundCache.WithThisCodeLocation()) defer txn.End() io.WriteString(w, "background transaction") time.Sleep(150 * time.Millisecond) }) http.HandleFunc("/background_log", func(w http.ResponseWriter, req *http.Request) { // Logs that occur outside of a transaction are classified as // background logs. app.RecordLog(newrelic.LogData{ Message: "Background Log Message", Severity: "info", }) io.WriteString(w, "A background log message was recorded") }) http.ListenAndServe(":8000", nil) } go-agent-3.42.0/v3/examples/short-lived-process/000077500000000000000000000000001510742411500213635ustar00rootroot00000000000000go-agent-3.42.0/v3/examples/short-lived-process/main.go000066400000000000000000000020101510742411500226270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Short Lived App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Wait for the application to connect. if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } // Do the tasks at hand. Perhaps record them using transactions and/or // custom events. tasks := []string{"white", "black", "red", "blue", "green", "yellow"} for _, task := range tasks { txn := app.StartTransaction("task") time.Sleep(10 * time.Millisecond) txn.End() app.RecordCustomEvent("task", map[string]interface{}{ "color": task, }) } // Shut down the application to flush data to New Relic. app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/go.mod000066400000000000000000000007261510742411500147440ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3 go 1.24 require ( google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) retract v3.22.0 // release process error corrected in v3.22.1 retract v3.25.0 // release process error corrected in v3.25.1 retract v3.34.0 // this release erronously referred to and invalid protobuf dependency retract v3.40.0 // this release erronously had deadlocks in utilization.go and incorrectly added aws-sdk-go to the go.mod filego-agent-3.42.0/v3/integrations/000077500000000000000000000000001510742411500163375ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/000077500000000000000000000000001510742411500210525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/000077500000000000000000000000001510742411500230305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/LICENSE.txt000066400000000000000000000264501510742411500246620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/Readme.md000066400000000000000000000030711510742411500245500ustar00rootroot00000000000000# New Relic Log Writer The logWriter library is an `io.Writer` that automatically integrates the latest New Relic Logs in Context features into the go standard library logger. When used as the `io.Writer` for log, this tool will collect log metrics, forward logs, and enrich logs depending on how your New Relic application is configured. This is the most complete and convenient way to to capture log data with New Relic in log. ## Usage Once your New Relic application has been created, create a logWriter instance. It must be passed an io.Writer, which is where the final log content will be written to, and a pointer to New Relic application. ```go writer := logWriter.New(os.Stdout, app) ``` If any errors occor while trying to decorate your log with New Relic metadata, it will fail silently and print your log message in its original, unedited form. If you want to see the error messages, then enable debug logging. This will print an error message in a new line after the original log message is printed. ```go writer.DebugLogging(true) ``` To capture log data in the context of a transaction, make a new logWriter with the `WithTransaction` or `WithContext` methods. If you have a pointer to a transaction, use the `WithTransaction()` function. ```go txn := app.StartTransaction("my transaction") defer txn.End() txnWriter := writer.WithTransaction(txn) ``` If you have a context with a transaction pointer in it, use the `WithContext()` function. ```go func ExampleHandler(w http.ResponseWriter, r *http.Request) { txnWriter := writer.WithContext(r.Context()) } ``` go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/example/000077500000000000000000000000001510742411500244635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/example/main.go000066400000000000000000000023111510742411500257330ustar00rootroot00000000000000package main import ( "log" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/logWriter" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("log writer example"), newrelic.ConfigFromEnvironment(), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), ) if err != nil { panic(err) } app.WaitForConnection(5 * time.Second) // Create a logWriter, then pass it to the log.Logger writer := logWriter.New(os.Stdout, app) logger := log.New(&writer, "Background: ", log.Default().Flags()) logger.Print("Hello world!") txnName := "Example Transaction" txn := app.StartTransaction(txnName) // Always create a new log object in order to avoid changing the context of the logger for // other threads that may be logging outside of this transaction txnLogger := log.New(writer.WithTransaction(txn), "Transaction: ", log.Default().Flags()) txnLogger.Printf("In transaction %s.", txnName) // simulate doing something time.Sleep(time.Microsecond * 100) txnLogger.Printf("Ending transaction %s.", txnName) txn.End() logger.Print("Goodbye!") app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/go.mod000066400000000000000000000005501510742411500241360ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/logWriter go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter => ../nrwriter replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/log-writer.go000066400000000000000000000030561510742411500254560ustar00rootroot00000000000000package logWriter import ( "context" "io" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) type LogWriter struct { w nrwriter.LogWriter } func init() { internal.TrackUsage("integration", "logcontext-v2", "logWriter") } // New creates a new LogWriter // output is the io.Writer destination that you want your log to be written to // app must be a vaild, non nil new relic Application func New(output io.Writer, app *newrelic.Application) LogWriter { return LogWriter{ w: nrwriter.New(output, app), } } // DebugLogging toggles whether error information should be printed to console. By default, this service // will fail silently. Enabling debug logging will print error messages on a new line after your log message. func (lw *LogWriter) DebugLogging(enabled bool) { lw.w.DebugLogging(enabled) } // WithTransaction creates a new LogWriter for a specific transactions func (lw *LogWriter) WithTransaction(txn *newrelic.Transaction) LogWriter { return LogWriter{w: lw.w.WithTransaction(txn)} } // WithContext creates a new LogWriter for the transaction inside of a context func (lw *LogWriter) WithContext(ctx context.Context) LogWriter { return LogWriter{w: lw.w.WithContext(ctx)} } // Write is a valid io.Writer method that will write the content of an enriched log to the output io.Writer func (lw LogWriter) Write(p []byte) (n int, err error) { enrichedLog := lw.w.EnrichLog(newrelic.LogData{Message: string(p)}, p) return lw.w.Write(enrichedLog) } go-agent-3.42.0/v3/integrations/logcontext-v2/logWriter/log-writer_test.go000066400000000000000000000032771510742411500265220ustar00rootroot00000000000000package logWriter import ( "bytes" "log" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var ( host, _ = sysinfo.Hostname() ) func TestE2E(t *testing.T) { app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) // Capture output in a buffer for testing buf := bytes.NewBuffer([]byte{}) // set up logger writer := New(buf, app.Application) logger := log.New(&writer, "My Prefix: ", log.Lshortfile) // configure log writer writer.DebugLogging(true) // create a log message logger.Print("Hello World!") logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: logcontext.LogSeverityUnknown, Message: "My Prefix: log-writer_test.go:37: Hello World!", Timestamp: internal.MatchAnyUnixMilli, }, }) } func BenchmarkWrite(b *testing.B) { app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) log := []byte(`{"time":1516134303,"level":"debug","message":"hello world"}`) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { a.Write(log) } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/000077500000000000000000000000001510742411500227255ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/LICENSE.txt000066400000000000000000000264501510742411500245570ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/example/000077500000000000000000000000001510742411500243605ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/example/main.go000066400000000000000000000033721510742411500256400ustar00rootroot00000000000000package main import ( "context" "log" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func doFunction2(txn *newrelic.Transaction, e *logrus.Entry) { defer txn.StartSegment("doFunction2").End() e.Error("In doFunction2") } func doFunction1(txn *newrelic.Transaction, e *logrus.Entry) { defer txn.StartSegment("doFunction1").End() e.Trace("In doFunction1") doFunction2(txn, e) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Logrus Logs In Context Example"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), // If you wanted to forward your logs using a log forwarder instead // newrelic.ConfigAppLogDecoratingEnabled(true), // newrelic.ConfigAppLogForwardingEnabled(false), ) if nil != err { log.Panic("Failed to create application", err) } log := logrus.New() log.SetLevel(logrus.TraceLevel) // Enable New Relic log decoration log.SetFormatter(nrlogrus.NewFormatter(app, &logrus.TextFormatter{})) log.Trace("waiting for connection to New Relic...") err = app.WaitForConnection(10 * time.Second) if nil != err { log.Panic("Failed to connect application", err) } defer app.Shutdown(10 * time.Second) log.Info("application connected to New Relic") log.Debug("Starting transaction now") txn := app.StartTransaction("main") // Add the transaction context to the logger. Only once this happens will // the logs be properly decorated with all required fields. e := log.WithContext(newrelic.NewContext(context.Background(), txn)) doFunction1(txn, e) e.Info("Ending transaction") txn.End() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/formatter.go000066400000000000000000000027351510742411500252660ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlogrus import ( "bytes" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func init() { internal.TrackUsage("integration", "logcontext-v2", "logrus") } // ContextFormatter is a `logrus.Formatter` that will format logs for sending // to New Relic. type ContextFormatter struct { app *newrelic.Application formatter logrus.Formatter } func NewFormatter(app *newrelic.Application, formatter logrus.Formatter) ContextFormatter { return ContextFormatter{ app: app, formatter: formatter, } } // Format renders a single log entry. func (f ContextFormatter) Format(e *logrus.Entry) ([]byte, error) { logData := newrelic.LogData{ Severity: e.Level.String(), Message: e.Message, Attributes: e.Data, } logBytes, err := f.formatter.Format(e) if err != nil { return nil, err } logBytes = bytes.TrimRight(logBytes, "\n") b := bytes.NewBuffer(logBytes) ctx := e.Context var txn *newrelic.Transaction if ctx != nil { txn = newrelic.FromContext(ctx) } if txn != nil { txn.RecordLog(logData) err := newrelic.EnrichLog(b, newrelic.FromTxn(txn)) if err != nil { return nil, err } } else { f.app.RecordLog(logData) err := newrelic.EnrichLog(b, newrelic.FromApp(f.app)) if err != nil { return nil, err } } b.WriteString("\n") return b.Bytes(), nil } go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/formatter_test.go000066400000000000000000000166341510742411500263300ustar00rootroot00000000000000package nrlogrus import ( "bytes" "context" "io" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/sirupsen/logrus" ) var ( host, _ = sysinfo.Hostname() ) func newTextLogger(out io.Writer, app *newrelic.Application) *logrus.Logger { l := logrus.New() l.Formatter = NewFormatter(app, &logrus.TextFormatter{ DisableColors: true, }) l.SetReportCaller(true) l.SetOutput(out) return l } func newJSONLogger(out io.Writer, app *newrelic.Application) *logrus.Logger { l := logrus.New() l.Formatter = NewFormatter(app, &logrus.JSONFormatter{}) l.SetReportCaller(true) l.SetOutput(out) return l } func BenchmarkFormatterLogic(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) formatter := NewFormatter(app.Application, &logrus.TextFormatter{}) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { _, err := formatter.Format(logrus.New().WithContext(context.Background())) if err != nil { b.Error(err) } } } func BenchmarkLogrusTextFormatter(b *testing.B) { log := newTextLogger(bytes.NewBuffer([]byte("")), nil) log.Formatter = new(logrus.TextFormatter) ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func BenchmarkFormattingWithOutTransaction(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) log := newTextLogger(bytes.NewBuffer([]byte("")), app.Application) ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func BenchmarkFormattingWithTransaction(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) txn := app.StartTransaction("TestLogDistributedTracingDisabled") out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) ctx := newrelic.NewContext(context.Background(), txn) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func TestBackgroundLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) message := "Hello World!" log.Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestBackgroundLogWithFields(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) message := "Hello World!" log.WithField("test field", []string{"a", "b"}).Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "test field": []string{"a", "b"}, }, }, }) } func TestJSONBackgroundLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newJSONLogger(out, app.Application) message := "Hello World!" log.Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestLogEmptyContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) message := "Hello World!" log.WithContext(context.Background()).Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestLogInContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) txn := app.StartTransaction("test txn") ctx := newrelic.NewContext(context.Background(), txn) message := "Hello World!" log.WithContext(ctx).Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) txn.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txn.GetLinkingMetadata().SpanID, TraceID: txn.GetLinkingMetadata().TraceID, }, }) txn.End() } func TestLogInContextWithFields(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTextLogger(out, app.Application) txn := app.StartTransaction("test txn") ctx := newrelic.NewContext(context.Background(), txn) message := "Hello World!" log.WithField("hi", 1).WithContext(ctx).Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) txn.ExpectLogEvents(t, []internal.WantLog{ { Severity: logrus.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txn.GetLinkingMetadata().SpanID, TraceID: txn.GetLinkingMetadata().TraceID, Attributes: map[string]interface{}{ "hi": 1, }, }, }) txn.End() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrlogrus/go.mod000066400000000000000000000003431510742411500240330ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/sirupsen/logrus v1.8.1 ) replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/000077500000000000000000000000001510742411500223565ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/LICENSE.txt000066400000000000000000000264501510742411500242100ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/attributes.go000066400000000000000000000044211510742411500250740ustar00rootroot00000000000000package nrslog import ( "log/slog" "maps" "strings" ) type attributeCache struct { preCompiledAttributes map[string]interface{} prefix string } func newAttributeCache() *attributeCache { return &attributeCache{ preCompiledAttributes: make(map[string]interface{}), prefix: "", } } func (c *attributeCache) clone() *attributeCache { return &attributeCache{ preCompiledAttributes: maps.Clone(c.preCompiledAttributes), prefix: c.prefix, } } func (c *attributeCache) copyPreCompiledAttributes() map[string]interface{} { return maps.Clone(c.preCompiledAttributes) } func (c *attributeCache) getPrefix() string { return c.prefix } // precompileGroup sets the group prefix for the cache created by a handler // precompileGroup call. This is used to avoid re-computing the group prefix // and should only ever be called on newly created caches and handlers. func (c *attributeCache) precompileGroup(group string) { if c.prefix != "" { c.prefix += "." } c.prefix += group } // precompileAttributes appends attributes to the cache created by a handler // WithAttrs call. This is used to avoid re-computing the with Attrs attributes // and should only ever be called on newly created caches and handlers. func (c *attributeCache) precompileAttributes(attrs []slog.Attr) { if len(attrs) == 0 { return } for _, a := range attrs { c.appendAttr(c.preCompiledAttributes, a, c.prefix) } } func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) { // Resolve the Attr's value before doing anything else. a.Value = a.Value.Resolve() // Ignore empty Attrs. if a.Equal(slog.Attr{}) { return } // majority of runtime spent allocating and copying strings group := strings.Builder{} group.Grow(len(groupPrefix) + len(a.Key) + 1) group.WriteString(groupPrefix) if a.Key != "" { if group.Len() > 0 { group.WriteByte('.') } group.WriteString(a.Key) } key := group.String() // If the Attr is a group, append its attributes if a.Value.Kind() == slog.KindGroup { attrs := a.Value.Group() // Ignore empty groups. if len(attrs) == 0 { return } for _, ga := range attrs { c.appendAttr(nrAttrs, ga, key) } return } // attr is an attribute nrAttrs[key] = a.Value.Any() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/config.go000066400000000000000000000045511510742411500241570ustar00rootroot00000000000000package nrslog import ( "time" "github.com/newrelic/go-agent/v3/newrelic" ) const updateFrequency = 1 * time.Minute // check infrequently because the go agent config is not expected to change --> cost 50-100 uS // 44% faster than checking the config on every log message type configCache struct { lastCheck time.Time // true if we have successfully gotten the config at least once to verify the agent is connected gotStartupConfig bool // true if the logs in context feature is enabled as well as either local decorating or forwarding enabled bool enrichLogs bool forwardLogs bool } func newConfigCache() *configCache { return &configCache{} } func (c *configCache) clone() *configCache { return &configCache{ lastCheck: c.lastCheck, gotStartupConfig: c.gotStartupConfig, enabled: c.enabled, enrichLogs: c.enrichLogs, forwardLogs: c.forwardLogs, } } func (c *configCache) shouldEnrichLog(app *newrelic.Application) bool { c.update(app) return c.enrichLogs } func (c *configCache) shouldForwardLogs(app *newrelic.Application) bool { c.update(app) return c.forwardLogs } // isEnabled returns true if the logs in context feature is enabled // as well as either local decorating or forwarding. func (c *configCache) isEnabled(app *newrelic.Application) bool { c.update(app) return c.enabled } // Note: this has a data race in async use cases, but it does not // cause logical errors, only cache misses. This is acceptable in // comparison to the cost of synchronization. func (c *configCache) update(app *newrelic.Application) { // do not get the config from agent if we have successfully gotten it before // and it has been less than updateFrequency since the last check. This is // because on startup, the agent will return a dummy config until it has // connected and received the real config. if c.gotStartupConfig && time.Since(c.lastCheck) < updateFrequency { return } config, ok := app.Config() if !ok { c.enrichLogs = false c.forwardLogs = false c.enabled = false return } c.gotStartupConfig = true c.enrichLogs = config.ApplicationLogging.LocalDecorating.Enabled && config.ApplicationLogging.Enabled c.forwardLogs = config.ApplicationLogging.Forwarding.Enabled && config.ApplicationLogging.Enabled c.enabled = config.ApplicationLogging.Enabled && (c.enrichLogs || c.forwardLogs) c.lastCheck = time.Now() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/example/000077500000000000000000000000001510742411500240115ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/example/main.go000066400000000000000000000021151510742411500252630ustar00rootroot00000000000000package main import ( "context" "log/slog" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("slog example app"), newrelic.ConfigFromEnvironment(), newrelic.ConfigAppLogEnabled(true), ) if err != nil { panic(err) } app.WaitForConnection(time.Second * 5) handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) nrHandler := nrslog.WrapHandler(app, handler) log := slog.New(nrHandler) log.Info("I am a log message") txn := app.StartTransaction("example transaction") ctx := newrelic.NewContext(context.Background(), txn) log.InfoContext(ctx, "I am a log inside a transaction with custom attributes!", slog.String("foo", "bar"), slog.Int("answer", 42), slog.Any("some_map", map[string]interface{}{"a": 1.0, "b": 2}), ) // pretend to do some work time.Sleep(500 * time.Millisecond) log.Warn("Uh oh, something important happened!") txn.End() log.Info("All Done!") app.Shutdown(time.Second * 10) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/go.mod000066400000000000000000000002711510742411500234640ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog go 1.24 require github.com/newrelic/go-agent/v3 v3.42.0 replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/handler.go000066400000000000000000000162221510742411500243250ustar00rootroot00000000000000package nrslog import ( "context" "errors" "log/slog" "strings" "time" "github.com/newrelic/go-agent/v3/newrelic" ) // NRHandler is an Slog handler that includes logic to implement // New Relic Logs in Context. Please always create a new handler // using the Wrap() or WrapHandler() functions to ensure proper // initialization. // // Note: shallow coppies of this handler may not duplicate underlying // datastructures, and may cause logical errors. Please use the Clone() // method to create deep coppies, or use the WithTransaction, WithAttrs, // or WithGroup methods to create new handlers with additional data. type NRHandler struct { *attributeCache *configCache *linkingCache // underlying object pointers handler slog.Handler app *newrelic.Application txn *newrelic.Transaction } // newHandler is an internal helper function to create a new NRHandler func newHandler(app *newrelic.Application, handler slog.Handler) *NRHandler { return &NRHandler{ handler: handler, attributeCache: newAttributeCache(), configCache: newConfigCache(), linkingCache: newLinkingCache(), app: app, } } // WrapHandler returns a new handler that is wrapped with New Relic tools to capture // log data based on your application's logs in context settings. // // Note: This function will silently error, and always return a valid handler // to avoid service disruptions. If you would prefer to handle errors when // wrapping your handler, use the Wrap() function instead. func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { if app == nil { return handler } if handler == nil { return handler } switch handler.(type) { case *NRHandler: return handler default: return newHandler(app, handler) } } var ErrNilApp = errors.New("New Relic application cannot be nil") var ErrNilHandler = errors.New("slog handler cannot be nil") var ErrAlreadyWrapped = errors.New("handler is already wrapped with a New Relic handler") // WrapHandler returns a new handler that is wrapped with New Relic tools to capture // log data based on your application's logs in context settings. func Wrap(app *newrelic.Application, handler slog.Handler) (*NRHandler, error) { if app == nil { return nil, ErrNilApp } if handler == nil { return nil, ErrNilHandler } if _, ok := handler.(*NRHandler); ok { return nil, ErrAlreadyWrapped } return newHandler(app, handler), nil } // New Returns a new slog.Logger object wrapped with a New Relic handler that controls // logs in context features. func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { return slog.New(WrapHandler(app, handler)) } // Clone creates a deep copy of the original handler, including a copy of all cached data // and the underlying handler. // // Note: application, transaction, and handler pointers will be coppied, but the underlying // data will not be duplicated. func (h *NRHandler) Clone() *NRHandler { return &NRHandler{ handler: h.handler, attributeCache: h.attributeCache.clone(), configCache: h.configCache.clone(), linkingCache: h.linkingCache.clone(), app: h.app, txn: h.txn, } } // WithTransaction returns a new handler that is configured to capture log data // and attribute it to a specific transaction. func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { h2 := h.Clone() h2.txn = txn return h2 } // Enabled reports whether the handler handles records at the given level. // The handler ignores records whose level is lower. func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { return h.handler.Enabled(ctx, lvl) } // Handle handles the Record. // It will only be called when Enabled returns true. func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { // exit quickly logs in context is disabled in the agent // to preserve resources if !h.isEnabled(h.app) { return h.handler.Handle(ctx, record) } // get transaction, preferring transaction from context nrTxn := h.txn ctxTxn := newrelic.FromContext(ctx) if ctxTxn != nil { nrTxn = ctxTxn } // if no app or txn, invoke underlying handler if h.app == nil && nrTxn == nil { return h.handler.Handle(ctx, record) } // timestamp must be sent to newrelic var timestamp int64 if record.Time.IsZero() { timestamp = time.Now().UnixMilli() } else { timestamp = record.Time.UnixMilli() } if h.shouldForwardLogs(h.app) { attrs := h.copyPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map coppies prefix := h.getPrefix() record.Attrs(func(attr slog.Attr) bool { h.appendAttr(attrs, attr, prefix) return true }) data := newrelic.LogData{ Severity: record.Level.String(), Timestamp: timestamp, Message: record.Message, Attributes: attrs, } if nrTxn != nil { nrTxn.RecordLog(data) } else { h.app.RecordLog(data) } } // enrich logs if h.shouldEnrichLog(h.app) { if nrTxn != nil { h.enrichRecord(nil, nrTxn, &record) } else { h.enrichRecord(h.app, nil, &record) } } return h.handler.Handle(ctx, record) } // WithAttrs returns a new Handler whose attributes consist of // both the receiver's attributes and the arguments. // // This wraps the WithAttrs of the underlying handler, and will not modify the // attributes slice in any way. func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } h2 := h.Clone() h2.handler = h.handler.WithAttrs(attrs) h2.precompileAttributes(attrs) return h2 } // WithGroup returns a new Handler with the given group appended to // the receiver's existing groups. // The keys of all subsequent attributes, whether added by With or in a // Record, should be qualified by the sequence of group names. // If the name is empty, WithGroup returns the receiver. func (h *NRHandler) WithGroup(name string) slog.Handler { if name == "" { return h } h2 := h.Clone() h2.handler = h.handler.WithGroup(name) h2.precompileGroup(name) return h2 } // WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic // transaction from context. // // Deprecated: this is a no-op func WithTransactionFromContext(handler slog.Handler) slog.Handler { return handler } // enrichRecord enriches the log record with New Relic linking metadata. // This is used to link logs to transactions in New Relic. // It will only be called when logs in context is enabled. // Transaction linking metadata is preferred over agent linking metadata. func (h *NRHandler) enrichRecord(app *newrelic.Application, txn *newrelic.Transaction, record *slog.Record) { var linkingMetadata string if txn != nil { linkingMetadata = nrLinkingString(h.getTransactionLinkingMetadata(txn)) } else if app != nil { linkingMetadata = nrLinkingString(h.getAgentLinkingMetadata(app)) } else { return } if linkingMetadata == "" { return } if record.Message == "" { record.Message = linkingMetadata return } newMessage := strings.Builder{} newMessage.WriteString(record.Message) if record.Message[len(record.Message)-1] != ' ' { newMessage.WriteString(" ") } newMessage.WriteString(linkingMetadata) record.Message = newMessage.String() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/handler_test.go000066400000000000000000000467541510742411500254010ustar00rootroot00000000000000package nrslog import ( "bytes" "context" "io" "log/slog" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var ( host, _ = sysinfo.Hostname() ) func TestHandler(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) message := "Hello World!" log.Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestWrap(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) handler := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}) type test struct { name string app *newrelic.Application h slog.Handler expectErr error expectHandler *NRHandler } tests := []test{ { name: "nil app", app: nil, h: handler, expectErr: ErrNilApp, expectHandler: nil, }, { name: "nil handler", app: app.Application, h: nil, expectErr: ErrNilHandler, expectHandler: nil, }, { name: "duplicated handler", app: app.Application, h: &NRHandler{}, expectErr: ErrAlreadyWrapped, expectHandler: nil, }, { name: "valid", app: app.Application, h: handler, expectErr: nil, expectHandler: &NRHandler{ app: app.Application, handler: handler, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h, err := Wrap(tt.app, tt.h) if err != tt.expectErr { t.Errorf("incorrect error return; expected: %v; got: %v", tt.expectErr, err) } if tt.expectHandler != nil { if h == nil { t.Errorf("expected handler to not be nil") } if tt.expectHandler.app != h.app { t.Errorf("expected: %v; got: %v", tt.expectHandler.app, h.app) } if tt.expectHandler.handler != h.handler { t.Errorf("expected: %v; got: %v", tt.expectHandler.handler, h.handler) } } else if h != nil { t.Errorf("expected handler to be nil") } }) } } func TestHandlerNilApp(t *testing.T) { out := bytes.NewBuffer([]byte{}) logger := New(nil, slog.NewTextHandler(out, &slog.HandlerOptions{})) message := "Hello World!" logger.Info(message) logStr := out.String() if strings.Contains(logStr, nrlinking) { t.Errorf(" %s should not contain %s", logStr, nrlinking) } if len(logStr) == 0 { t.Errorf("log string should not be empty") } } func TestJSONHandler(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) handler := JSONHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) message := "Hello World!" log.Info(message) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestHandlerTransactions(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) message := "Hello World!" handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) txn := app.Application.StartTransaction("my txn") txninfo := txn.GetLinkingMetadata() txnLogger := WithTransaction(txn, log) txnLogger.Info(message) backgroundMsg := "this is a background message" log.Debug(backgroundMsg) txn.End() logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txninfo.SpanID, TraceID: txninfo.TraceID, }, }) } func TestHandlerTransactionCtx(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) message := "Hello World!" handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) txn := app.Application.StartTransaction("my txn") ctx := newrelic.NewContext(context.Background(), txn) txninfo := txn.GetLinkingMetadata() txnLogger := WithContext(ctx, log) txnLogger.Info(message) backgroundMsg := "this is a background message" log.Debug(backgroundMsg) txn.End() logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txninfo.SpanID, TraceID: txninfo.TraceID, }, }) } func TestHandlerTransactionsAndBackground(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) message := "Hello World!" messageTxn := "Hello Transaction!" messageBackground := "Hello Background!" handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) log.Info(message) txn := app.Application.StartTransaction("my txn") txninfo := txn.GetLinkingMetadata() txnLogger := WithTransaction(txn, log) txnLogger.Info(messageTxn) log.Warn(messageBackground) txn.End() app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, { Severity: slog.LevelWarn.String(), Message: messageBackground, Timestamp: internal.MatchAnyUnixMilli, }, { Severity: slog.LevelInfo.String(), Message: messageTxn, Timestamp: internal.MatchAnyUnixMilli, SpanID: txninfo.SpanID, TraceID: txninfo.TraceID, }, }) } func TestWithAttributes(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(false), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) message := "Hello World!" log = log.With(slog.String("string key", "val"), slog.Int("int key", 1)) log.Info(message) txn := app.StartTransaction("hi") txnLog := WithTransaction(txn, log) txnLog.Info(message, slog.Duration("duration", 3*time.Second)) data := txn.GetLinkingMetadata() txn.End() additionalAttrs := slog.String("additional", "attr") log = log.WithGroup("group1") log.Info(message, additionalAttrs) log = log.WithGroup("group2") log.Info(message, additionalAttrs) log = log.With(additionalAttrs) log.Info(message) expectInt := int64(1) app.ExpectLogEvents(t, []internal.WantLog{ { Attributes: map[string]interface{}{ "string key": "val", "int key": expectInt, }, Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, { Attributes: map[string]interface{}{ "string key": "val", "int key": expectInt, "duration": time.Duration(3 * time.Second), }, Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: data.SpanID, TraceID: data.TraceID, }, { Attributes: map[string]interface{}{ "string key": "val", "int key": expectInt, "group1.additional": "attr", }, Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, { Attributes: map[string]interface{}{ "string key": "val", "int key": expectInt, "group1.group2.additional": "attr", }, Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, { Attributes: map[string]interface{}{ "string key": "val", "int key": expectInt, "group1.group2.additional": "attr", }, Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestWithAttributesFromContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) writer := &bytes.Buffer{} log := New(app.Application, slog.NewTextHandler(writer, &slog.HandlerOptions{})) log.Info("I am a log message") logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, EntityName: integrationsupport.SampleAppName, Hostname: host, }) // purge the buffer writer.Reset() txn := app.StartTransaction("example transaction") ctx := newrelic.NewContext(context.Background(), txn) log.InfoContext(ctx, "I am a log inside a transaction with custom attributes!", slog.String("foo", "bar"), slog.Int("answer", 42), ) metadata := txn.GetTraceMetadata() txn.End() logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, EntityName: integrationsupport.SampleAppName, Hostname: host, TraceID: metadata.TraceID, SpanID: metadata.SpanID, }) writer.Reset() gLog := log.WithGroup("group1") gLog.Info("I am a log message inside a group", slog.String("foo", "bar"), slog.Int("answer", 42)) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: "I am a log message", Timestamp: internal.MatchAnyUnixMilli, }, { Severity: slog.LevelInfo.String(), Message: "I am a log inside a transaction with custom attributes!", Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "foo": "bar", "answer": int64(42), }, TraceID: metadata.TraceID, SpanID: metadata.SpanID, }, { Severity: slog.LevelInfo.String(), Message: "I am a log message inside a group", Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "group1.foo": "bar", "group1.answer": int64(42), }, }, }) } // Ensure deprecation compatibility func TestDeprecatedWithTransactionFromContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) message := "Hello World!" handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(WithTransactionFromContext(handler)) txn := app.Application.StartTransaction("my txn") ctx := newrelic.NewContext(context.Background(), txn) txninfo := txn.GetLinkingMetadata() log.InfoContext(ctx, message) txn.End() logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txninfo.SpanID, TraceID: txninfo.TraceID, }, }) } func TestWithComplexAttributeOrGroup(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) message := "Hello World!" attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) log := New(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) log.Info(message, attr) fooLog := log.WithGroup("foo") fooLog.Info(message, attr) log.With(attr).WithGroup("group3").With(slog.String("key3", "val3")).Info(message) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "group.key": "val", "group.group2.key2": "val2", }, }, { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "foo.group.key": "val", "foo.group.group2.key2": "val2", }, }, { Severity: slog.LevelInfo.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "group.key": "val", "group.group2.key2": "val2", "group3.key3": "val3", }, }, }) } func TestAppendAttr(t *testing.T) { h := &NRHandler{} nrAttrs := map[string]interface{}{} attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) h.appendAttr(nrAttrs, attr, "") if len(nrAttrs) != 2 { t.Errorf("expected 2 attributes, got %d", len(nrAttrs)) } entry1, ok := nrAttrs["group.key"] if !ok { t.Errorf("expected group.key to be in the map") } if entry1 != "val" { t.Errorf("expected value of 'group.key' to be val, got '%s'", entry1) } entry2, ok := nrAttrs["group.group2.key2"] if !ok { t.Errorf("expected group.group2.key2 to be in the map") } if entry2 != "val2" { t.Errorf("expected value of 'group.group2.key2' to be val2, got '%s'", entry2) } } func TestAppendAttrWithGroupPrefix(t *testing.T) { h := &NRHandler{} nrAttrs := map[string]interface{}{} attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) h.appendAttr(nrAttrs, attr, "prefix") if len(nrAttrs) != 2 { t.Errorf("expected 2 attributes, got %d", len(nrAttrs)) } entry1, ok := nrAttrs["prefix.group.key"] if !ok { t.Errorf("expected group.key to be in the map") } if entry1 != "val" { t.Errorf("expected value of 'group.key' to be val, got '%s'", entry1) } entry2, ok := nrAttrs["prefix.group.group2.key2"] if !ok { t.Errorf("expected group.group2.key2 to be in the map") } if entry2 != "val2" { t.Errorf("expected value of 'group.group2.key2' to be val2, got '%s'", entry2) } } func TestHandlerZeroTime(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{})) handler.Handle(context.Background(), slog.Record{ Level: slog.LevelInfo, Message: "Hello World!", Time: time.Time{}, }) logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), Message: "Hello World!", Timestamp: internal.MatchAnyUnixMilli, }, }) } func BenchmarkDefaultHandler(b *testing.B) { handler := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}) record := slog.Record{ Time: time.Now(), Message: "Hello World!", Level: slog.LevelInfo, } ctx := context.Background() record.AddAttrs(slog.String("key", "val"), slog.Int("int", 1)) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { handler.Handle(ctx, record) } } func BenchmarkHandler(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(false), newrelic.ConfigAppLogForwardingEnabled(true), ) handler, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) txn := app.Application.StartTransaction("my txn") defer txn.End() ctx := newrelic.NewContext(context.Background(), txn) record := slog.Record{ Time: time.Now(), Message: "Hello World!", Level: slog.LevelInfo, } record.AddAttrs(slog.String("key", "val"), slog.Int("int", 1)) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { handler.Handle(ctx, record) } } // the maps are costing so much here func BenchmarkAppendAttribute(b *testing.B) { h := &NRHandler{} nrAttrs := map[string]interface{}{} attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { h.appendAttr(nrAttrs, attr, "") } } func BenchmarkEnrichLog(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) txn := app.Application.StartTransaction("my txn") defer txn.End() record := slog.Record{} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { nrLinking := bytes.NewBuffer([]byte{}) err := newrelic.EnrichLog(nrLinking, newrelic.FromTxn(txn)) if err == nil { record.AddAttrs(slog.String("newrelic", nrLinking.String())) } } } func BenchmarkLinkingStringEnrichment(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) h, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) txn := app.Application.StartTransaction("my txn") defer txn.End() record := slog.Record{} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { h.enrichRecord(app.Application, nil, &record) } } func BenchmarkLinkingString(b *testing.B) { md := newrelic.LinkingMetadata{ EntityGUID: "entityGUID", Hostname: "hostname", TraceID: "traceID", SpanID: "spanID", EntityName: "entityName", } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { nrLinkingString(md) } } func BenchmarkShouldEnrichLog(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) h, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) txn := app.Application.StartTransaction("my txn") defer txn.End() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { h.shouldEnrichLog(app.Application) } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/linking.go000066400000000000000000000062211510742411500243410ustar00rootroot00000000000000package nrslog import ( "strings" "github.com/newrelic/go-agent/v3/newrelic" ) type linkingCache struct { loaded bool entityGUID string entityName string hostname string } func newLinkingCache() *linkingCache { return &linkingCache{} } func (data *linkingCache) clone() *linkingCache { return &linkingCache{ entityGUID: data.entityGUID, entityName: data.entityName, hostname: data.hostname, } } // getAgentLinkingMetadata returns the linking metadata for the agent. // we save a lot of time making calls to the go agent by caching the linking metadata // which will never change during the lifetime of the agent. // // This returns a shallow copy of the cached metadata object // 50% faster than calling GetLinkingMetadata() on every log message // // worst case: data race --> performance degrades to the cost of querying newrelic.Application.GetLinkingMetadata() func (cache *linkingCache) getAgentLinkingMetadata(app *newrelic.Application) newrelic.LinkingMetadata { // entityGUID will be empty until the agent has connected if !cache.loaded { metadata := app.GetLinkingMetadata() cache.entityGUID = metadata.EntityGUID cache.entityName = metadata.EntityName cache.hostname = metadata.Hostname if cache.entityGUID != "" { cache.loaded = true } return metadata } return newrelic.LinkingMetadata{ EntityGUID: cache.entityGUID, EntityName: cache.entityName, Hostname: cache.hostname, } } // getTransactionLinkingMetadata returns the linking metadata for a transaction. // we save a lot of time making calls to the go agent by caching the linking metadata // which will never change during the lifetime of the transaction. This still needs to // query for the trace and span IDs, but this is much cheaper than getting the linking metadata. // // This returns a shallow copy of the cached metadata object func (cache *linkingCache) getTransactionLinkingMetadata(txn *newrelic.Transaction) newrelic.LinkingMetadata { if !cache.loaded { metadata := txn.GetLinkingMetadata() // marginally more expensive cache.entityGUID = metadata.EntityGUID cache.entityName = metadata.EntityName cache.hostname = metadata.Hostname if cache.entityGUID != "" { cache.loaded = true } return metadata } traceData := txn.GetTraceMetadata() return newrelic.LinkingMetadata{ EntityGUID: cache.entityGUID, EntityName: cache.entityName, Hostname: cache.hostname, TraceID: traceData.TraceID, SpanID: traceData.SpanID, } } const nrlinking = "NR-LINKING" // nrLinkingString returns a string that represents the linking metadata func nrLinkingString(data newrelic.LinkingMetadata) string { if data.EntityGUID == "" { return "" } len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) str := strings.Builder{} str.Grow(len) // only 1 alloc str.WriteString(nrlinking) str.WriteByte('|') str.WriteString(data.EntityGUID) str.WriteByte('|') str.WriteString(data.Hostname) str.WriteByte('|') str.WriteString(data.TraceID) str.WriteByte('|') str.WriteString(data.SpanID) str.WriteByte('|') str.WriteString(data.EntityName) str.WriteByte('|') return str.String() } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/linking_test.go000066400000000000000000000154761510742411500254140ustar00rootroot00000000000000package nrslog import ( "os" "reflect" "testing" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func Test_linkingCache_getAgentLinkingMetadata(t *testing.T) { hostname, _ := os.Hostname() app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) md := app.GetLinkingMetadata() tests := []struct { name string obj *linkingCache app *newrelic.Application wantMetadata newrelic.LinkingMetadata wantCache linkingCache }{ { name: "empty cache", obj: &linkingCache{}, app: app.Application, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: md.EntityGUID, EntityName: "my app", Hostname: hostname, }, wantCache: linkingCache{ loaded: true, entityGUID: md.EntityGUID, entityName: "my app", hostname: hostname, }, }, { name: "loaded cache preserved", obj: &linkingCache{ loaded: true, entityGUID: "test entity GUID", entityName: "test app", hostname: "test hostname", }, app: app.Application, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: "test entity GUID", EntityName: "test app", Hostname: "test hostname", }, wantCache: linkingCache{ loaded: true, entityGUID: "test entity GUID", entityName: "test app", hostname: "test hostname", }, }, { name: "cache replaced when GUID is empty", obj: &linkingCache{ entityGUID: "", entityName: "test app", hostname: "test hostname", }, app: app.Application, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: md.EntityGUID, EntityName: "my app", Hostname: hostname, }, wantCache: linkingCache{ loaded: true, entityGUID: md.EntityGUID, entityName: "my app", hostname: hostname, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.obj.getAgentLinkingMetadata(tt.app) if got.EntityGUID != tt.wantMetadata.EntityGUID { t.Errorf("got incorrect entity GUID for agent = %v, want %v", got.EntityGUID, tt.wantMetadata.EntityGUID) } if got.EntityName != tt.wantMetadata.EntityName { t.Errorf("got incorrect entity name for agent = %v, want %v", got.EntityName, tt.wantMetadata.EntityName) } if got.Hostname != tt.wantMetadata.Hostname { t.Errorf("got incorrect hostname for agent = %v, want %v", got.Hostname, tt.wantMetadata.Hostname) } if got.TraceID != tt.wantMetadata.TraceID { t.Errorf("got incorrect trace ID for transaction = %v, want %v", got.TraceID, tt.wantMetadata.TraceID) } if got.SpanID != tt.wantMetadata.SpanID { t.Errorf("got incorrect span ID for transaction = %v, want %v", got.SpanID, tt.wantMetadata.SpanID) } if !reflect.DeepEqual(tt.obj, &tt.wantCache) { t.Errorf("linkingCache state is incorrect = %v, want %v", tt.obj, tt.wantCache) } }) } } func Test_linkingCache_getTransactionLinkingMetadata(t *testing.T) { hostname, _ := os.Hostname() app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) txn := app.StartTransaction("txn") defer txn.End() md := txn.GetLinkingMetadata() tests := []struct { name string obj *linkingCache txn *newrelic.Transaction wantMetadata newrelic.LinkingMetadata wantCache linkingCache }{ { name: "empty cache", obj: &linkingCache{}, txn: txn, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: md.EntityGUID, EntityName: "my app", Hostname: hostname, TraceID: md.TraceID, SpanID: md.SpanID, }, wantCache: linkingCache{ loaded: true, entityGUID: md.EntityGUID, entityName: "my app", hostname: hostname, }, }, { name: "cache preserved when loaded", obj: &linkingCache{ loaded: true, entityGUID: "test entity GUID", entityName: "test app", hostname: "test hostname", }, txn: txn, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: "test entity GUID", EntityName: "test app", Hostname: "test hostname", TraceID: md.TraceID, SpanID: md.SpanID, }, wantCache: linkingCache{ loaded: true, entityGUID: "test entity GUID", entityName: "test app", hostname: "test hostname", }, }, { name: "cache replaced not fully loaded", obj: &linkingCache{ loaded: false, entityGUID: "", entityName: "test app", hostname: "test hostname", }, txn: txn, wantMetadata: newrelic.LinkingMetadata{ EntityGUID: md.EntityGUID, EntityName: "my app", Hostname: hostname, TraceID: md.TraceID, SpanID: md.SpanID, }, wantCache: linkingCache{ loaded: true, entityGUID: md.EntityGUID, entityName: "my app", hostname: hostname, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.obj.getTransactionLinkingMetadata(tt.txn) if got.EntityGUID != tt.wantMetadata.EntityGUID { t.Errorf("got incorrect entity GUID for agent = %v, want %v", got.EntityGUID, tt.wantMetadata.EntityGUID) } if got.EntityName != tt.wantMetadata.EntityName { t.Errorf("got incorrect entity name for agent = %v, want %v", got.EntityName, tt.wantMetadata.EntityName) } if got.Hostname != tt.wantMetadata.Hostname { t.Errorf("got incorrect hostname for agent = %v, want %v", got.Hostname, tt.wantMetadata.Hostname) } if got.TraceID != tt.wantMetadata.TraceID { t.Errorf("got incorrect trace ID for transaction = %v, want %v", got.TraceID, tt.wantMetadata.TraceID) } if got.SpanID != tt.wantMetadata.SpanID { t.Errorf("got incorrect span ID for transaction = %v, want %v", got.SpanID, tt.wantMetadata.SpanID) } if !reflect.DeepEqual(tt.obj, &tt.wantCache) { t.Errorf("linkingCache state is incorrect = %+v, want %+v", tt.obj, tt.wantCache) } }) } } func BenchmarkGetAgentLinkingMetadata(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) cache := &linkingCache{} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { cache.getAgentLinkingMetadata(app.Application) } } func BenchmarkGetTransactionLinkingMetadata(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) txn := app.StartTransaction("txn") defer txn.End() // cache := &linkingCache{} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { txn.GetTraceMetadata() } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrslog/wrap.go000066400000000000000000000045001510742411500236550ustar00rootroot00000000000000package nrslog import ( "context" "io" "log/slog" "github.com/newrelic/go-agent/v3/newrelic" ) // TextHandler creates a wrapped Slog TextHandler, enabling it to both automatically capture logs // and to enrich logs locally depending on your logs in context configuration in your New Relic // application. // // Deprecated: Use WrapHandler() instead. func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) slog.Handler { return WrapHandler(app, slog.NewTextHandler(w, opts)) } // JSONHandler creates a wrapped Slog JSONHandler, enabling it to both automatically capture logs // and to enrich logs locally depending on your logs in context configuration in your New Relic // application. // // Deprecated: Use WrapHandler() instead. func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) slog.Handler { return WrapHandler(app, slog.NewJSONHandler(w, opts)) } // WithTransaction creates a new Slog Logger object to be used for logging within a given // transaction it its found in a context. Creating a transaction logger can have a performance // benefit when transactions are long running, and have a high log volume in comparison to // reading transactions from context on every log message. // // Note: transaction contexts can also be passed to the logger without creating a new // logger using logger.InfoContext() or similar commands. func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { if ctx == nil { return logger } txn := newrelic.FromContext(ctx) return WithTransaction(txn, logger) } // WithTransaction creates a new Slog Logger object to be used for logging // within a given transaction. Creating a transaction logger can have a performance // benefit when transactions are long running, and have a high log volume in comparison to // reading transactions from context on every log message. // // Note: transaction contexts can also be passed to the logger without creating a new // logger using logger.InfoContext() or similar commands. func WithTransaction(txn *newrelic.Transaction, logger *slog.Logger) *slog.Logger { if txn == nil || logger == nil { return logger } h := logger.Handler() switch nrHandler := h.(type) { case *NRHandler: txnHandler := nrHandler.WithTransaction(txn) return slog.New(txnHandler) default: return logger } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrwriter/000077500000000000000000000000001510742411500227265ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrwriter/LICENSE.txt000066400000000000000000000264501510742411500245600ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/nrwriter/go.mod000066400000000000000000000002731510742411500240360ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter go 1.24 require github.com/newrelic/go-agent/v3 v3.42.0 replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/nrwriter/nrwriter.go000066400000000000000000000050041510742411500251300ustar00rootroot00000000000000// nrwriter is a library of common code that handles capturing and sending New Relic logs in context data // from any io.Writer. This module should not be used as a standalone integration for logs in context. // // See github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter for an example of how // to use this library. package nrwriter import ( "bytes" "context" "io" "github.com/newrelic/go-agent/v3/newrelic" ) // LogWriter is an io.Writer that captures log data for use with New Relic Logs in Context type LogWriter struct { debug bool out io.Writer app *newrelic.Application txn *newrelic.Transaction } // New creates a new NewRelicWriter Object // output is the io.Writer destination that you want your log to be written to // app must be a vaild, non nil new relic Application func New(output io.Writer, app *newrelic.Application) LogWriter { return LogWriter{ out: output, app: app, } } // DebugLogging enables or disables debug error messages being written in the IO output. // By default, the nrwriter debug logging is set to false and will fail silently func (b *LogWriter) DebugLogging(enabled bool) { b.debug = enabled } // WithTransaction duplicates the current NewRelicWriter and sets the transaction to txn func (b *LogWriter) WithTransaction(txn *newrelic.Transaction) LogWriter { return LogWriter{ out: b.out, app: b.app, debug: b.debug, txn: txn, } } // WithTransaction duplicates the current NewRelicWriter and sets the transaction to the transaction parsed from ctx func (b *LogWriter) WithContext(ctx context.Context) LogWriter { txn := newrelic.FromContext(ctx) return LogWriter{ out: b.out, app: b.app, debug: b.debug, txn: txn, } } // EnrichLog attempts to enrich a log with New Relic linking metadata. If it fails, // it will return the original log line unless debug=true, otherwise it will print // an error on a following line. func (b *LogWriter) EnrichLog(data newrelic.LogData, p []byte) []byte { logLine := bytes.TrimRight(p, "\n") buf := bytes.NewBuffer(logLine) var enrichErr error if b.txn != nil { b.txn.RecordLog(data) enrichErr = newrelic.EnrichLog(buf, newrelic.FromTxn(b.txn)) } else { b.app.RecordLog(data) enrichErr = newrelic.EnrichLog(buf, newrelic.FromApp(b.app)) } if b.debug && enrichErr != nil { buf.WriteString("\n") buf.WriteString(enrichErr.Error()) } buf.WriteString("\n") return buf.Bytes() } // Write implements io.Write func (b LogWriter) Write(p []byte) (n int, err error) { return b.out.Write(p) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrwriter/nrwriter_test.go000066400000000000000000000125621510742411500261760ustar00rootroot00000000000000package nrwriter import ( "bytes" "context" "io" "strings" "testing" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var ( host, _ = sysinfo.Hostname() ) const ( benchmarkMsg = "This is a test log message" ) func BenchmarkEnrichLog(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) a := New(io.Discard, app.Application) a.DebugLogging(true) buf := bytes.NewBuffer([]byte(benchmarkMsg)) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { a.EnrichLog(newrelic.LogData{}, buf.Bytes()) } } const ( logMessageWithNewline = "This is a log message with a newline\n" logMessageWithoutNewline = "This is a log message without a newline" logMessageWithSpace = "This is a log message with a space at the end \n" logMessageWithoutNewlineAndWithSpace = "This is a log message without a newline " nrlinking = "NR-LINKING" ) func TestLogSpacingAndNewlines(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) lines := []string{ logMessageWithNewline, logMessageWithSpace, logMessageWithoutNewline, logMessageWithoutNewlineAndWithSpace, } for _, line := range lines { buf.Write(a.EnrichLog(newrelic.LogData{}, []byte(line))) log := buf.String() // verify there is a single newline at the end of the log line if strings.Count(log, "\n") != 1 { t.Errorf("Expected a single log line ending with one newline, instead got: %s", log) } substrings := strings.Split(log, nrlinking) if len(substrings) != 2 { t.Errorf("Expected %s metadata but log line was not decorated: %s", nrlinking, log) } else { whitespace := countTrailingWhitespace(substrings[0]) if whitespace != 1 { t.Errorf("Expecting a single whitespace separating log line from %s, got %d: %s", nrlinking, whitespace, log) } } buf.Reset() } } func countTrailingWhitespace(str string) int { count := 0 for i := len(str) - 1; i >= 0; i-- { if str[i] == ' ' { count++ } else { break } } return count } func TestBackgroundLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) a.Write(a.EnrichLog(newrelic.LogData{}, []byte(logMessageWithNewline))) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) } func TestTransactionLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) txn := app.StartTransaction("test transaction") b := a.WithTransaction(txn) b.Write(b.EnrichLog(newrelic.LogData{}, []byte(logMessageWithNewline))) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) txn.End() } func TestContextLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) txn := app.StartTransaction("test transaction") ctx := newrelic.NewContext(context.Background(), txn) b := a.WithContext(ctx) b.Write(b.EnrichLog(newrelic.LogData{}, []byte(logMessageWithNewline))) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) txn.End() } func TestNilContextLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) b := a.WithContext(nil) // verify that when context is nil, log is enriched with application data b.Write(b.EnrichLog(newrelic.LogData{}, []byte(logMessageWithNewline))) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) buf.Reset() // verify that when context is empty, log is enriched with application data c := a.WithContext(context.Background()) c.Write(c.EnrichLog(newrelic.LogData{}, []byte(logMessageWithNewline))) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/000077500000000000000000000000001510742411500222045ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/LICENSE.txt000066400000000000000000000264501510742411500240360ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/example/000077500000000000000000000000001510742411500236375ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/example/main.go000066400000000000000000000034661510742411500251230ustar00rootroot00000000000000package main import ( "errors" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzap" "github.com/newrelic/go-agent/v3/newrelic" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("nrzerolog example"), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigFromEnvironment(), // This is enabled by default. if disabled, the attributes will be marshalled at harvest time. newrelic.ConfigZapAttributesEncoder(false), ) if err != nil { panic(err) } app.WaitForConnection(5 * time.Second) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), zap.InfoLevel) backgroundCore, err := nrzap.WrapBackgroundCore(core, app) if err != nil && err != nrzap.ErrNilApp { panic(err) } backgroundLogger := zap.New(backgroundCore) backgroundLogger.Info("this is a background log message with fields test", zap.Any("foo", 3.14)) txn := app.StartTransaction("nrzap example transaction") txnCore, err := nrzap.WrapTransactionCore(core, txn) if err != nil && err != nrzap.ErrNilTxn { panic(err) } txnLogger := zap.New(txnCore) txnLogger.Info("this is a transaction log message with custom fields", zap.String("zapstring", "region-test-2"), zap.Int("zapint", 123), zap.Duration("zapduration", 200*time.Millisecond), zap.Bool("zapbool", true), zap.Object("zapobject", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error { enc.AddString("foo", "bar") return nil })), zap.Any("zapmap", map[string]any{"pi": 3.14, "duration": 2 * time.Second}), ) err = errors.New("OW! an error occurred") txnLogger.Error("this is an error log message", zap.Error(err)) txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/go.mod000066400000000000000000000003261510742411500233130ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzap go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 go.uber.org/zap v1.24.0 ) replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/nrzap.go000066400000000000000000000157471510742411500237030ustar00rootroot00000000000000package nrzap import ( "errors" "math" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func init() { internal.TrackUsage("integration", "logcontext-v2", "zap") } // NewRelicZapCore implements zap.Core type NewRelicZapCore struct { fields []zap.Field core zapcore.Core nr newrelicApplicationState } // newrelicApplicationState is a private struct that stores newrelic application data // for automatic behind the scenes log collection logic. type newrelicApplicationState struct { app *newrelic.Application txn *newrelic.Transaction } // Helper function that converts zap fields to a map of string interface func convertFieldWithMapEncoder(fields []zap.Field) map[string]interface{} { attributes := make(map[string]interface{}) for _, field := range fields { enc := zapcore.NewMapObjectEncoder() field.AddTo(enc) for key, value := range enc.Fields { // Format time.Duration values as strings if durationVal, ok := value.(time.Duration); ok { attributes[key] = durationVal.String() } else { attributes[key] = value } } } return attributes } func convertFieldsAtHarvestTime(fields []zap.Field) map[string]interface{} { attributes := make(map[string]interface{}) for _, field := range fields { if field.Interface != nil { // Handles ErrorType fields if field.Type == zapcore.ErrorType { attributes[field.Key] = field.Interface.(error).Error() } else { // Handles all interface types attributes[field.Key] = field.Interface } } else if field.String != "" { // Check if the field is a string and doesn't contain an interface attributes[field.Key] = field.String } else { // Float Types if field.Type == zapcore.Float32Type { attributes[field.Key] = math.Float32frombits(uint32(field.Integer)) continue } else if field.Type == zapcore.Float64Type { attributes[field.Key] = math.Float64frombits(uint64(field.Integer)) continue } // Bool Type if field.Type == zapcore.BoolType { field.Interface = field.Integer == 1 attributes[field.Key] = field.Interface } else { // Integer Types attributes[field.Key] = field.Integer } } } return attributes } // internal handler function to manage writing a log to the new relic application func (nr *newrelicApplicationState) recordLog(entry zapcore.Entry, fields []zap.Field) { attributes := map[string]interface{}{} cfg, _ := nr.app.Config() // Check if the attributes should be frontloaded or marshalled at harvest time if cfg.ApplicationLogging.ZapLogger.AttributesFrontloaded { attributes = convertFieldWithMapEncoder(fields) } else { attributes = convertFieldsAtHarvestTime(fields) } data := newrelic.LogData{ Timestamp: entry.Time.UnixMilli(), Severity: entry.Level.String(), Message: entry.Message, Attributes: attributes, } if nr.txn != nil { nr.txn.RecordLog(data) } else if nr.app != nil { nr.app.RecordLog(data) } } var ( // ErrNilZapcore is an error caused by calling a WrapXCore function on a nil zapcore.Core object ErrNilZapcore = errors.New("cannot wrap nil zapcore.Core object") // ErrNilApp is an error caused by calling WrapBackgroundCore with a nil newrelic.Application ErrNilApp = errors.New("wrapped a zapcore.Core with a nil New Relic application; logs will not be captured") // ErrNilTxn is an error caused by calling WrapTransactionCore with a nil newrelic.Transaction ErrNilTxn = errors.New("wrapped a zapcore.Core with a nil New Relic transaction; logs will not be captured") ) // NewBackgroundCore creates a new NewRelicZapCore object, which is a wrapped zapcore.Core object. This wrapped object // captures background logs in context and sends them to New Relic. // // Errors will be returned if the zapcore object is nil, or if the application is nil. It is up to the user to decide // how to handle the case where the newrelic.Application is nil. // In the case that the newrelic.Application is nil, a valid NewRelicZapCore object will still be returned. // // Please note that, while enriched context is added to log data forwarded to New Relic telemetry, // it is not added to the local log data itself. This is due to the specific way zap logs are // efficiently integrated into the New Relic agent logs in context. Local log decoration for zap // logs requires additional custom code. func WrapBackgroundCore(core zapcore.Core, app *newrelic.Application) (*NewRelicZapCore, error) { if core == nil { return nil, ErrNilZapcore } var err error if app == nil { err = ErrNilApp } return &NewRelicZapCore{ core: core, nr: newrelicApplicationState{ app: app, }, }, err } // WrapTransactionCore creates a new NewRelicZapCore object, which is a wrapped zapcore.Core object. This wrapped object // captures logs in context of a transaction and sends them to New Relic. // // Errors will be returned if the zapcore object is nil, or if the application is nil. It is up to the user to decide // how to handle the case where the newrelic.Transaction is nil. // In the case that the newrelic.Application is nil, a valid NewRelicZapCore object will still be returned. // // Please note that, while enriched context is added to log data forwarded to New Relic telemetry, // it is not added to the local log data itself. This is due to the specific way zap logs are // efficiently integrated into the New Relic agent logs in context. Local log decoration for zap // logs requires additional custom code. func WrapTransactionCore(core zapcore.Core, txn *newrelic.Transaction) (zapcore.Core, error) { if core == nil { return nil, ErrNilZapcore } var err error if txn == nil { err = ErrNilTxn } return &NewRelicZapCore{ core: core, nr: newrelicApplicationState{ txn: txn, }, }, err } // With makes a copy of a NewRelicZapCore with new zap.Fields. It calls zapcore.With() on the zap core object // then makes a deepcopy of the NewRelicApplicationState object so the original // object can be deallocated when it's no longer in scope. func (c *NewRelicZapCore) With(fields []zap.Field) zapcore.Core { return &NewRelicZapCore{ core: c.core.With(fields), fields: append(fields, c.fields...), nr: newrelicApplicationState{ c.nr.app, c.nr.txn, }, } } // Check simply calls zapcore.Check on the Core object. func (c *NewRelicZapCore) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { ce := c.core.Check(entry, checkedEntry) ce.AddCore(entry, c) return ce } // Write wraps zapcore.Write and captures the log entry and sends that data to New Relic. func (c *NewRelicZapCore) Write(entry zapcore.Entry, fields []zap.Field) error { allFields := append(fields, c.fields...) c.nr.recordLog(entry, allFields) return nil } // Sync simply calls zapcore.Sync on the Core object. func (c *NewRelicZapCore) Sync() error { return c.core.Sync() } // Enabled simply calls zapcore.Enabled on the zapcore.Level passed to it. func (c *NewRelicZapCore) Enabled(level zapcore.Level) bool { return c.core.Enabled(level) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzap/nrzap_test.go000066400000000000000000000244621510742411500247340ustar00rootroot00000000000000package nrzap import ( "errors" "io" "os" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func TestBackgroundLogger(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapBackgroundCore(core, app.Application) if err != nil { t.Error(err) } logger := zap.New(wrappedCore) err = errors.New("this is a test error") msg := "this is a test error message" // for background logging: logger.Error(msg, zap.Error(err), zap.String("test-key", "test-val")) logger.Sync() app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zap.ErrorLevel.String(), Message: msg, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestBackgroundLoggerSugared(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), zap.InfoLevel) backgroundCore, err := WrapBackgroundCore(core, app.Application) if err != nil && err != ErrNilApp { t.Fatal(err) } logger := zap.New(backgroundCore).Sugar() err = errors.New("this is a test error") msg := "this is a test error message" // for background logging: logger.Error(msg, zap.Error(err), zap.String("test-key", "test-val")) logger.Sync() app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zap.ErrorLevel.String(), Message: `this is a test error message{error 26 0 this is a test error} {test-key 15 0 test-val }`, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestBackgroundLoggerNilApp(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapBackgroundCore(core, nil) if err != ErrNilApp { t.Error(err) } if wrappedCore == nil { t.Error("when the app is nil, the core returned should still be valid") } logger := zap.New(wrappedCore) err = errors.New("this is a test error") msg := "this is a test error message" // for background logging: logger.Error(msg, zap.Error(err), zap.String("test-key", "test-val")) logger.Sync() // Expect no log events in logger without app in core app.ExpectLogEvents(t, []internal.WantLog{}) } func TestTransactionLogger(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) txn := app.StartTransaction("test transaction") txnMetadata := txn.GetTraceMetadata() core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapTransactionCore(core, txn) if err != nil { t.Error(err) } logger := zap.New(wrappedCore) err = errors.New("this is a test error") msg := "this is a test error message" // for background logging: logger.Error(msg, zap.Error(err), zap.String("test-key", "test-val")) logger.Sync() // ensure txn gets written to an event and logs get released txn.End() app.ExpectLogEvents(t, []internal.WantLog{ { Attributes: map[string]interface{}{ "test-key": "test-val", "error": "this is a test error", }, Severity: zap.ErrorLevel.String(), Message: msg, Timestamp: internal.MatchAnyUnixMilli, TraceID: txnMetadata.TraceID, SpanID: txnMetadata.SpanID, }, }) } func TestTransactionLoggerWithFields(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), newrelic.ConfigZapAttributesEncoder(true), ) txn := app.StartTransaction("test transaction") txnMetadata := txn.GetTraceMetadata() core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapTransactionCore(core, txn) if err != nil { t.Error(err) } wrappedCore = wrappedCore.With([]zapcore.Field{ zap.String("foo", "bar"), }) logger := zap.New(wrappedCore) msg := "this is a test info message" // for background logging: logger.Info(msg, zap.String("region", "region-test-2"), zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), zap.Duration("duration", 1*time.Second), zap.Int("int", 123), zap.Bool("bool", true), ) logger.Sync() // ensure txn gets written to an event and logs get released txn.End() app.ExpectLogEvents(t, []internal.WantLog{ { Attributes: map[string]interface{}{ "region": "region-test-2", "anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}, "duration": "1s", "int": int64(123), "bool": true, "foo": "bar", }, Severity: zap.InfoLevel.String(), Message: msg, Timestamp: internal.MatchAnyUnixMilli, TraceID: txnMetadata.TraceID, SpanID: txnMetadata.SpanID, }, }) } func TestTransactionLoggerWithFieldsAtHarvestTime(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), newrelic.ConfigZapAttributesEncoder(false), ) txn := app.StartTransaction("test transaction") txnMetadata := txn.GetTraceMetadata() core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapTransactionCore(core, txn) if err != nil { t.Error(err) } logger := zap.New(wrappedCore) msg := "this is a test info message" // for background logging: logger.Info(msg, zap.String("region", "region-test-2"), zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), zap.Duration("duration", 1*time.Second), zap.Int("int", 123), zap.Bool("bool", true), ) logger.Sync() // ensure txn gets written to an event and logs get released txn.End() app.ExpectLogEvents(t, []internal.WantLog{ { Attributes: map[string]interface{}{ "region": "region-test-2", "anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}, "duration": "1s", "int": int64(123), "bool": true, }, Severity: zap.InfoLevel.String(), Message: msg, Timestamp: internal.MatchAnyUnixMilli, TraceID: txnMetadata.TraceID, SpanID: txnMetadata.SpanID, }, }) } func TestTransactionLoggerNilTxn(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) txn := app.StartTransaction("test transaction") // txnMetadata := txn.GetTraceMetadata() core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapTransactionCore(core, nil) if err != ErrNilTxn { t.Error(err) } if wrappedCore == nil { t.Error("when the txn is nil, the core returned should still be valid") } logger := zap.New(wrappedCore) err = errors.New("this is a test error") msg := "this is a test error message" // for background logging: logger.Error(msg, zap.Error(err), zap.String("test-key", "test-val")) logger.Sync() // ensure txn gets written to an event and logs get released txn.End() // no data should be captured when nil txn passed to wrapped logger app.ExpectLogEvents(t, []internal.WantLog{}) } func TestWith(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) wrappedCore, err := WrapBackgroundCore(core, app.Application) if err != nil { t.Error(err) } newCore := wrappedCore.With([]zapcore.Field{}) if newCore == core { t.Error("core was not coppied during With() operaion") } } func BenchmarkZapBaseline(b *testing.B) { core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(io.Discard), zap.InfoLevel) logger := zap.New(core) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { logger.Info("this is a test message") } } func BenchmarkFieldConversion(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { convertFieldWithMapEncoder([]zap.Field{ zap.String("test-key", "test-val"), zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), }) } } func BenchmarkFieldUnmarshalling(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { convertFieldsAtHarvestTime([]zap.Field{ zap.String("test-key", "test-val"), zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), }) } } func BenchmarkZapWithAttribute(b *testing.B) { core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(io.Discard), zap.InfoLevel) logger := zap.New(core) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { logger.Info("this is a test message", zap.Any("test-key", "test-val")) } } func BenchmarkZapWrappedCore(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(io.Discard), zap.InfoLevel) wrappedCore, err := WrapBackgroundCore(core, app.Application) if err != nil { b.Error(err) } logger := zap.New(wrappedCore) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { logger.Info("this is a test message") } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/000077500000000000000000000000001510742411500230735ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/LICENSE.txt000066400000000000000000000264501510742411500247250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/Readme.md000066400000000000000000000070451510742411500246200ustar00rootroot00000000000000# Zerolog In Context This plugin for zerolog implements the logs in context tooling for the go agent. This hook function can be added to any zerolog logger, and will automatically collect the log data from zerolog, and send it to New Relic through the go agent. The following Logging features are supported by this plugin in the current release: | Logging Feature | Supported | | ------- | --------- | | Forwarding | :heavy_check_mark: | | Metrics | :heavy_check_mark: | | Enrichment | :x: | ## Installation The nrzerolog plugin, and the go-agent need to be integrated into your code in order to use this tool. Make sure to set `newrelic.ConfigAppLogForwardingEnabled(true)` in your config settings for the application. This will enable log forwarding in the go agent. If you want to disable metrics, set `newrelic.ConfigAppLogMetricsEnabled(false),`. Note that the agent sets the default number of logs per harverst cycle to 10000, but that number may be reduced by the server. You can manually set this number by setting `newrelic.ConfigAppLogForwardingMaxSamplesStored(123),`. The following example will shows how to install and set up your code to send logs to new relic from zerolog. ```go import ( "github.com/rs/zerolog" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzerolog" ) func main() { // Initialize a zerolog logger baseLogger := zerolog.New(os.Stdout) app, err := newrelic.NewApplication( newrelic.ConfigFromEnvironment(), newrelic.ConfigAppName("NRZerolog Example"), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), ) if err != nil { fmt.Println(err) os.Exit(1) } // Send logs to New Relic outside of a transaction nrHook := nrzerolog.NewRelicHook{ App: app, } // Wrap logger with New Relic Hook nrLogger := baseLogger.Hook(nrHook) nrLogger.Info().Msg("Hello World") // Send logs to New Relic inside of a transaction txn := app.StartTransaction("My Transaction") ctx := newrelic.NewContext(context.Background(), txn) nrTxnHook := nrzerolog.NewRelicHook{ App: app, Context: ctx, } txnLogger := baseLogger.Hook(nrTxnHook) txnLogger.Debug().Msg("This is a transaction log") txn.End() } ``` ## Usage Please enable the agent to ingest your logs by calling newrelic.ConfigAppLogForwardingEnabled(true), when setting up your application. This is not enabled by default. This integration for the zerolog logging frameworks uses a built in feature of the zerolog framework called hook functions. Zerolog loggers can be modified to have hook functions run on them before each time a write is executed. When a logger is hooked, meaning a hook function was added to that logger with the Hook() funciton, a copy of that logger is created with those changes. Note that zerolog will *never* attempt to verify that any hook functions have not been not duplicated, or that fields are not repeated in any way. As a result, we recommend that you create a base logger that is configured in the way you prefer to use zerolog. Then you create hooked loggers to send log data to New Relic from that base logger. The plugin captures the log level, and the message from zerolog. It will also collect distributed tracing data from your transaction context. At the moment the hook function is called in zerolog, a timestamp will be generated for your log. In most cases, this timestamp will be the same as the time posted in the zerolog log message, however it is possible that there could be a slight offset depending on the the performance of your system. go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/example/000077500000000000000000000000001510742411500245265ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/example/main.go000066400000000000000000000020461510742411500260030ustar00rootroot00000000000000package main import ( "context" "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzerolog" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) func main() { baseLogger := zerolog.New(os.Stdout) app, err := newrelic.NewApplication( newrelic.ConfigFromEnvironment(), newrelic.ConfigAppName("NRZerolog Example"), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), ) if err != nil { fmt.Println(err) os.Exit(1) } app.WaitForConnection(5 * time.Second) nrHook := nrzerolog.NewRelicHook{ App: app, } nrLogger := baseLogger.Hook(nrHook) nrLogger.Info().Msg("Hello World") // With transaction context txn := app.StartTransaction("My Transaction") ctx := newrelic.NewContext(context.Background(), txn) nrTxnHook := nrzerolog.NewRelicHook{ App: app, Context: ctx, } txnLogger := baseLogger.Hook(nrTxnHook) txnLogger.Debug().Msg("This is a transaction log") txn.End() nrLogger.Info().Msg("Goodbye") app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/go.mod000066400000000000000000000003401510742411500241760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzerolog go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/rs/zerolog v1.26.1 ) replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/hook.go000066400000000000000000000013261510742411500243640ustar00rootroot00000000000000package nrzerolog import ( "context" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) func init() { internal.TrackUsage("integration", "logcontext-v2", "zerolog") } type NewRelicHook struct { App *newrelic.Application Context context.Context } func (h NewRelicHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { var txn *newrelic.Transaction if h.Context != nil { txn = newrelic.FromContext(h.Context) } logLevel := "" if level != zerolog.NoLevel { logLevel = level.String() } data := newrelic.LogData{ Severity: logLevel, Message: msg, } if txn != nil { txn.RecordLog(data) } else { h.App.RecordLog(data) } } go-agent-3.42.0/v3/integrations/logcontext-v2/nrzerolog/hook_test.go000066400000000000000000000132321510742411500254220ustar00rootroot00000000000000package nrzerolog import ( "bytes" "context" "io" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/rs/zerolog" ) func newLogger(out io.Writer, app *newrelic.Application) zerolog.Logger { logger := zerolog.New(out) return logger.Hook(NewRelicHook{ App: app, }) } func newTxnLogger(out io.Writer, app *newrelic.Application, ctx context.Context) zerolog.Logger { logger := zerolog.New(out) return logger.Hook(NewRelicHook{ App: app, Context: ctx, }) } func BenchmarkZerolog(b *testing.B) { log := zerolog.New(bytes.NewBuffer([]byte(""))) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.Info().Msg("This is a test log") } } func BenchmarkZerologLoggingDisabled(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogEnabled(false)) log := newLogger(bytes.NewBuffer([]byte("")), app.Application) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.Info().Msg("This is a test log") } } func BenchmarkZerologLogForwarding(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogForwardingEnabled(true)) log := newLogger(bytes.NewBuffer([]byte("")), app.Application) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.Info().Msg("This is a test log") } } /* func BenchmarkFormattingWithOutTransaction(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) log := newLogger(bytes.NewBuffer([]byte("")), app.Application) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.Info().Msg("Hello World!") } } func BenchmarkFormattingWithTransaction(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true)) txn := app.StartTransaction("TestLogDistributedTracingDisabled") defer txn.End() out := bytes.NewBuffer([]byte{}) ctx := newrelic.NewContext(context.Background(), txn) log := newTxnLogger(out, app.Application, ctx) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.Info().Msg("Hello World!") } } */ func TestBackgroundLog(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newLogger(out, app.Application) message := "Hello World!" log.Info().Msg(message) // Un-comment when local decorating enabled /* logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) */ app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestLogEmptyContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTxnLogger(out, app.Application, context.Background()) message := "Hello World!" log.Info().Msg(message) // Un-comment when local decorating enabled /* logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) */ app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestLogDebugLevel(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) log := newTxnLogger(out, app.Application, context.Background()) message := "Hello World!" log.Print(message) // Un-comment when local decorating enabled /* logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) */ app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.DebugLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestLogInContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) out := bytes.NewBuffer([]byte{}) txn := app.StartTransaction("test txn") ctx := newrelic.NewContext(context.Background(), txn) log := newTxnLogger(out, app.Application, ctx) message := "Hello World!" log.Info().Msg(message) // Un-comment when local decorating enabled /* logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) */ txn.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.InfoLevel.String(), Message: message, Timestamp: internal.MatchAnyUnixMilli, SpanID: txn.GetLinkingMetadata().SpanID, TraceID: txn.GetLinkingMetadata().TraceID, }, }) txn.End() } go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/000077500000000000000000000000001510742411500237305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/LICENSE.txt000066400000000000000000000264501510742411500255620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/Readme.md000066400000000000000000000030771510742411500254560ustar00rootroot00000000000000# New Relic Zerolog Writer The zerolog-writer library is an `io.Writer` that automatically integrates the latest New Relic Logs in Context features into Zerolog. When used as the `io.Writer` for zerolog, this tool will collect log metrics, forward logs, and enrich logs depending on how your New Relic application is configured. This is the most complete and convenient way to to capture log data with New Relic in Zerolog. ## Usage Once your New Relic application has been created, create a ZerologWriter instance. It must be passed an io.Writer, which is where the final log content will be written to, and a pointer to New Relic application. ```go writer := zerologWriter.New(os.Stdout, app) ``` If any errors occor while trying to decorate your log with New Relic metadata, it will fail silently and print your log message in its original, unedited form. If you want to see the error messages, then enable debug logging. This will print an error message in a new line after the original log message is printed. ```go writer.DebugLogging(true) ``` To capture log data in the context of a transaction, make a new ZerologWriter with the `WithTransaction` or `WithContext` methods. If you have a pointer to a transaction, use the `WithTransaction()` function. ```go txn := app.StartTransaction("my transaction") defer txn.End() txnWriter := writer.WithTransaction(txn) ``` If you have a context with a transaction pointer in it, use the `WithContext()` function. ```go func ExampleHandler(w http.ResponseWriter, r *http.Request) { txnWriter := writer.WithContext(r.Context()) } ``` go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/example/000077500000000000000000000000001510742411500253635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/example/main.go000066400000000000000000000021471510742411500266420ustar00rootroot00000000000000package main import ( "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("nrwriter log writer example"), newrelic.ConfigFromEnvironment(), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigAppLogDecoratingEnabled(true), ) if err != nil { panic(err) } app.WaitForConnection(5 * time.Second) writer := zerologWriter.New(os.Stdout, app) logger := zerolog.New(writer) logger.Print("Application connected to New Relic.") txnName := "Example Transaction" txn := app.StartTransaction(txnName) // Always create a new logger in order to avoid changing the context of the logger for // other threads that may be logging outside of this transaction txnLogger := logger.Output(writer.WithTransaction(txn)) txnLogger.Printf("In transaction %s.", txnName) // simulate doing something time.Sleep(time.Microsecond * 100) txnLogger.Printf("Ending transaction %s.", txnName) txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/go.mod000066400000000000000000000006131510742411500250360ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.2 github.com/rs/zerolog v1.27.0 ) replace github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter => ../nrwriter replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/zerolog-writer.go000066400000000000000000000155271510742411500272640ustar00rootroot00000000000000package zerologWriter import ( "bytes" "context" "io" "strings" "time" "unicode" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) type ZerologWriter struct { w nrwriter.LogWriter } func init() { internal.TrackUsage("integration", "logcontext-v2", "zerologWriter") } // New creates a new NewRelicWriter Object // output is the io.Writer destination that you want your log to be written to // app must be a vaild, non nil new relic Application func New(output io.Writer, app *newrelic.Application) ZerologWriter { return ZerologWriter{ w: nrwriter.New(output, app), } } // DebugLogging toggles whether error information should be printed to console. By default, this service // will fail silently. Enabling debug logging will print error messages on a new line after your log message. func (zw *ZerologWriter) DebugLogging(enabled bool) { zw.w.DebugLogging(enabled) } // WithTransaction creates a new ZerologWriter for a specific transactions func (zw *ZerologWriter) WithTransaction(txn *newrelic.Transaction) ZerologWriter { return ZerologWriter{w: zw.w.WithTransaction(txn)} } // WithContext creates a new ZerologWriter for the transaction inside of a context func (zw *ZerologWriter) WithContext(ctx context.Context) ZerologWriter { return ZerologWriter{w: zw.w.WithContext(ctx)} } // Write is a valid io.Writer method that will write the content of an enriched log to the output io.Writer func (zw ZerologWriter) Write(p []byte) (n int, err error) { data := parseJSONLogData(p) enrichedLog := zw.w.EnrichLog(data, p) return zw.w.Write(enrichedLog) } func parseJSONLogData(log []byte) newrelic.LogData { // For this iteration of the tool, the entire log gets captured as the message data := newrelic.LogData{} data.Message = string(log) data.Timestamp = time.Now().UnixMilli() i := skipPastSpaces(log, 0) if i < 0 || i >= len(log) || log[i] != '{' { return data } i++ for i < len(log)-1 { // get key; always a string field key, valStart := getKey(log, i) if valStart < 0 { return data } var next int // NOTE: depending on the key, the type of field the value is can differ switch key { case zerolog.LevelFieldName: data.Severity, next = getStringValue(log, valStart) case zerolog.ErrorStackFieldName: _, next = getStackTrace(log, valStart) default: if i >= len(log)-1 { return data } // TODO: once we update the logging spec to support custom attributes, capture these if isStringValue(log, valStart) { _, next = getStringValue(log, valStart) } else if isNumberValue(log, valStart) { _, next = getNumberValue(log, valStart) } else { return data } } if next == -1 { return data } i = next } return data } func isStringValue(p []byte, indx int) bool { if indx = skipPastSpaces(p, indx); indx < 0 { return false } return p[indx] == '"' } func isNumberValue(p []byte, indx int) bool { if indx = skipPastSpaces(p, indx); indx < 0 { return false } // unicode.IsDigit isn't sufficient here because JSON numbers can start with a sign too return unicode.IsDigit(rune(p[indx])) || p[indx] == '-' } // zerolog keys are always JSON strings func getKey(p []byte, indx int) (string, int) { value := strings.Builder{} i := skipPastSpaces(p, indx) if i < 0 || i >= len(p) || p[i] != '"' { return "", -1 } // parse value of string field literalNext := false for i++; i < len(p); i++ { if literalNext { value.WriteByte(p[i]) literalNext = false continue } if p[i] == '\\' { value.WriteByte(p[i]) literalNext = true continue } if p[i] == '"' { // found end of key. Now look for the colon separator for i++; i < len(p); i++ { if p[i] == ':' && i+1 < len(p) { return value.String(), i + 1 } if p[i] != ' ' && p[i] != '\t' { break } } // Oh oh. if we got here, there wasn't a colon, or there wasn't a value after it, or // something showed up between the end of the key and the colon that wasn't a space. // In any of those cases, we didn't find the key of a key/value pair. return "", -1 } else { value.WriteByte(p[i]) } } return "", -1 } /* func isEOL(p []byte, i int) bool { for ; i < len(p); i++ { if p[i] == ' ' || p[i] == '\t' { continue } if p[i] == '}' { // nothing but space to the end from here? for i++; i < len(p); i++ { if p[i] != ' ' && p[i] != '\t' && p[i] != '\r' && p[i] != '\n' { return false // nope, that wasn't the end of the string } } return true } } return false } */ func skipPastSpaces(p []byte, i int) int { for ; i < len(p); i++ { if p[i] != ' ' && p[i] != '\t' && p[i] != '\r' && p[i] != '\n' { return i } } return -1 } func getStringValue(p []byte, indx int) (string, int) { value := strings.Builder{} // skip to start of string i := skipPastSpaces(p, indx) if i < 0 || i >= len(p) || p[i] != '"' { return "", -1 } // parse value of string field literalNext := false for i++; i < len(p); i++ { if literalNext { value.WriteByte(p[i]) literalNext = false continue } if p[i] == '\\' { value.WriteByte(p[i]) literalNext = true continue } if p[i] == '"' { // end of string. search past the comma so we can find the following key (if any) later. if i = skipPastSpaces(p, i+1); i < 0 || i >= len(p) { return value.String(), -1 } if p[i] == ',' { if i+1 < len(p) { return value.String(), i + 1 } return value.String(), -1 } return value.String(), -1 } value.WriteByte(p[i]) } return "", -1 } func getNumberValue(p []byte, indx int) (string, int) { value := strings.Builder{} // parse value of string field i := skipPastSpaces(p, indx) if i < 0 { return "", -1 } // JSON numeric values contain digits, '.', '-', 'e' for ; i < len(p) && bytes.IndexByte([]byte("0123456789-+eE."), p[i]) >= 0; i++ { value.WriteByte(p[i]) } i = skipPastSpaces(p, i) if i > 0 && i+1 < len(p) && p[i] == ',' { return value.String(), i + 1 } return value.String(), -1 } func getStackTrace(p []byte, indx int) (string, int) { value := strings.Builder{} i := skipPastSpaces(p, indx) if i < 0 || i >= len(p) || p[i] != '[' { return "", -1 } // the stack trace is everything from '[' to the next ']'. // TODO: this is a little naïve and we may need to consider parsing // the data inbetween more carefully. To date, we haven't seen a case // where that is necessary, and prefer not to impact performance of the // system by doing the extra processing, but we can revisit that later // if necessary. for ; i < len(p); i++ { if p[i] == ']' { value.WriteByte(p[i]) i = skipPastSpaces(p, i) if i > 0 && i+1 < len(p) && p[i] == ',' { return value.String(), i + 1 } return value.String(), -1 } else { value.WriteByte(p[i]) } } return value.String(), -1 } go-agent-3.42.0/v3/integrations/logcontext-v2/zerologWriter/zerolog-writer_test.go000066400000000000000000000243111510742411500303120ustar00rootroot00000000000000package zerologWriter import ( "bytes" "context" "io" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/rs/zerolog" ) var ( host, _ = sysinfo.Hostname() ) func TestParseLogData(t *testing.T) { type logTest struct { log string levelKey string expect newrelic.LogData } tests := []logTest{ { `{"time":1516134303,"level":"debug","message":"hello world"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516134303,"level":"debug","message":"hello world"}` + "\n", Severity: "debug", }, }, { `{"time":1516134303,"level":"info","message":"hello world"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516134303,"level":"info","message":"hello world"}` + "\n", Severity: "info", }, }, { `{"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}` + "\n", Severity: "fatal", }, }, { `{"time":1516134303,"hi":"info","message":"hello world"}` + "\n", "hi", newrelic.LogData{ Message: `{"time":1516134303,"hi":"info","message":"hello world"}` + "\n", Severity: "info", }, }, { `{"time":1516134303,"level":"debug","message":"hello, world"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516134303,"level":"debug","message":"hello, world"}` + "\n", Severity: "debug", }, }, { `{"time":1516134303,"level":"debug","message":"hello, world { thing }"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516134303,"level":"debug","message":"hello, world { thing }"}` + "\n", Severity: "debug", }, }, { `{"time":1516134303,"level":"debug","message":"hello, world \"{ thing \"}"}` + "\n", "level", newrelic.LogData{ Message: `{"time":1516134303,"level":"debug","message":"hello, world \"{ thing \"}"}` + "\n", Severity: "debug", }, }, { `{"message":"hello, world \"{ thing \"}","time":1516134303,"level":"debug"}` + "\n", "level", newrelic.LogData{ Message: `{"message":"hello, world \"{ thing \"}","time":1516134303,"level":"debug"}` + "\n", Severity: "debug", }, }, { // basic stack trace test `{"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}` + "\n", "level", newrelic.LogData{ Message: `{"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}` + "\n", Severity: "error", }, }, { // Tests that code can handle a stack trace, even if its at EOL `{"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}]}` + "\n", "level", newrelic.LogData{ Message: `{"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}]}` + "\n", Severity: "error", }, }, { `{"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}` + "\n", "level", newrelic.LogData{ Message: `{"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}` + "\n", Severity: "debug", }, }, { `{"Scale":"833 cents" , "Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere","level":"debug"}` + "\n", "level", newrelic.LogData{ Message: `{"Scale":"833 cents" , "Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere","level":"debug"}` + "\n", Severity: "debug", }, }, /* regression test case from issue 955 by MarioCarrion */ { `{"level":"info","message":"\"value\","}` + "\n", "level", newrelic.LogData{ Message: `{"level":"info","message":"\"value\","}` + "\n", Severity: "info", }, }, { `{"level":"info","message":","}` + "\n", "level", newrelic.LogData{ Message: `{"level":"info","message":","}` + "\n", Severity: "info", }, }, /* end of issue 955 test case */ } for _, test := range tests { if test.levelKey != "" { zerolog.LevelFieldName = test.levelKey } val := parseJSONLogData([]byte(test.log)) if val.Message != test.expect.Message { parserTestError(t, "Message", val.Message, test.expect.Message, test.log) } if val.Severity != test.expect.Severity { parserTestError(t, "Severity", val.Severity, test.expect.Severity, test.log) } zerolog.LevelFieldName = "level" } } func TestParseLogDataEscapes(t *testing.T) { type logTest struct { logMessage string levelKey string expectMessage string } tests := []logTest{ { "escape quote,\"", "info", `{"level":"info","message":"escape quote,\""}`, }, { "escape quote,\", hi", "info", `{"level":"info","message":"escape quote,\", hi"}`, }, { "escape quote,\",\" hi", "info", `{"level":"info","message":"escape quote,\",\" hi"}`, }, { "escape bracket,\"}\n hi", "info", `{"level":"info","message":"escape bracket,\"}\n hi"}`, }, } app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogForwardingEnabled(true), ) writer := New(io.Discard, app.Application) writer.DebugLogging(true) logger := zerolog.New(writer) wantLog := []internal.WantLog{} for _, test := range tests { logger.Info().Msg(test.logMessage) wantLog = append(wantLog, internal.WantLog{ Severity: zerolog.LevelInfoValue, Message: test.expectMessage, Timestamp: internal.MatchAnyUnixMilli, }) } app.ExpectLogEvents(t, wantLog) } func parserTestError(t *testing.T, field, actual, expect, input string) { t.Errorf("The parsed %s does not match the expected message: parsed \"%s\" expected \"%s\"\nFailed on input: %s", field, actual, expect, input) } func TestE2E(t *testing.T) { app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) logger := zerolog.New(a) logger.Info().Msg("Hello World!") logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.LevelInfoValue, Message: `{"level":"info","message":"Hello World!"}`, Timestamp: internal.MatchAnyUnixMilli, }, }) } func TestE2EWithContext(t *testing.T) { app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) txn := app.Application.StartTransaction("test") ctx := newrelic.NewContext(context.Background(), txn) txnWriter := a.WithContext(ctx) logger := zerolog.New(txnWriter) logger.Info().Msg("Hello World!") traceID := txn.GetLinkingMetadata().TraceID spanID := txn.GetLinkingMetadata().SpanID txn.End() // must end txn to dump logs into harvest logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.LevelInfoValue, Message: `{"level":"info","message":"Hello World!"}`, Timestamp: internal.MatchAnyUnixMilli, TraceID: traceID, SpanID: spanID, }, }) } func TestE2EWithTxn(t *testing.T) { app := integrationsupport.NewTestApp( integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) buf := bytes.NewBuffer([]byte{}) a := New(buf, app.Application) a.DebugLogging(true) txn := app.Application.StartTransaction("test") // create logger with txn context txnWriter := a.WithTransaction(txn) logger := zerolog.New(txnWriter) logger.Info().Msg("Hello World!") traceID := txn.GetLinkingMetadata().TraceID spanID := txn.GetLinkingMetadata().SpanID txn.End() // must end txn to dump logs into harvest logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ EntityGUID: integrationsupport.TestEntityGUID, Hostname: host, EntityName: integrationsupport.SampleAppName, }) app.ExpectLogEvents(t, []internal.WantLog{ { Severity: zerolog.LevelInfoValue, Message: `{"level":"info","message":"Hello World!"}`, Timestamp: internal.MatchAnyUnixMilli, TraceID: traceID, SpanID: spanID, }, }) } func BenchmarkParseLogLevel(b *testing.B) { log := []byte(`{"time":1516134303,"level":"debug","message":"hello world"}`) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { parseJSONLogData(log) } } go-agent-3.42.0/v3/integrations/logcontext/000077500000000000000000000000001510742411500205255ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext/README.md000066400000000000000000000007531510742411500220110ustar00rootroot00000000000000# v3/integrations/logcontext [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext) Logs in Context. Each directory represents a different logging plugin. Plugins allow you to add the context required to your log messages so you can see linking in the APM UI. For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext). go-agent-3.42.0/v3/integrations/logcontext/logcontext.go000066400000000000000000000034311510742411500232430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package logcontext facilitates adding New Relic context to your logs. // // Adding New Relic context to your logs will allow you to see links between // your events and traces in APM and your logs. If you are using a logging // framework that does not already have a New Relic plugin for log decoration, // use this package to manually add logging context. // // See https://github.com/newrelic/newrelic-exporter-specs/tree/master/logging // for a complete specification. package logcontext import newrelic "github.com/newrelic/go-agent/v3/newrelic" // Keys used for logging context JSON. const ( KeyFile = "file.name" KeyLevel = "log.level" KeyLine = "line.number" KeyMessage = "message" KeyMethod = "method.name" KeyTimestamp = "timestamp" KeyTraceID = "trace.id" KeySpanID = "span.id" KeyEntityName = "entity.name" KeyEntityType = "entity.type" KeyEntityGUID = "entity.guid" KeyHostname = "hostname" ) func metadataMapField(m map[string]interface{}, key, val string) { if val != "" { m[key] = val } } // AddLinkingMetadata adds the LinkingMetadata into a map. Only non-empty // string fields are included in the map. The specific key names facilitate // agent logs in context. These keys are: "trace.id", "span.id", // "entity.name", "entity.type", "entity.guid", and "hostname". func AddLinkingMetadata(m map[string]interface{}, md newrelic.LinkingMetadata) { metadataMapField(m, KeyTraceID, md.TraceID) metadataMapField(m, KeySpanID, md.SpanID) metadataMapField(m, KeyEntityName, md.EntityName) metadataMapField(m, KeyEntityType, md.EntityType) metadataMapField(m, KeyEntityGUID, md.EntityGUID) metadataMapField(m, KeyHostname, md.Hostname) } go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/000077500000000000000000000000001510742411500236175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/LICENSE.txt000066400000000000000000000264501510742411500254510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/README.md000066400000000000000000000010371510742411500250770ustar00rootroot00000000000000# v3/integrations/logcontext/nrlogrusplugin [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin) Package `nrlogrusplugin` decorates logs for sending to the New Relic backend. ```go import "github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin). go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/example/000077500000000000000000000000001510742411500252525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/example/main.go000066400000000000000000000032661510742411500265340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func doFunction2(txn *newrelic.Transaction, e *logrus.Entry) { defer txn.StartSegment("doFunction2").End() e.Error("In doFunction2") } func doFunction1(txn *newrelic.Transaction, e *logrus.Entry) { defer txn.StartSegment("doFunction1").End() e.Trace("In doFunction1") doFunction2(txn, e) } func main() { log := logrus.New() // To enable New Relic log decoration, use the // nrlogrusplugin.ContextFormatter{} log.SetFormatter(nrlogrusplugin.ContextFormatter{}) log.SetLevel(logrus.TraceLevel) log.Debug("Logger created") app, err := newrelic.NewApplication( newrelic.ConfigAppName("Logrus Log Decoration"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDistributedTracerEnabled(true), ) if nil != err { log.Panic("Failed to create application", err) } log.Debug("Application created, waiting for connection") err = app.WaitForConnection(10 * time.Second) if nil != err { log.Panic("Failed to connect application", err) } log.Info("Application connected") defer app.Shutdown(10 * time.Second) log.Debug("Starting transaction now") txn := app.StartTransaction("main") // Add the transaction context to the logger. Only once this happens will // the logs be properly decorated with all required fields. e := log.WithContext(newrelic.NewContext(context.Background(), txn)) doFunction1(txn, e) e.Info("Ending transaction") txn.End() } go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/go.mod000066400000000000000000000006101510742411500247220ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin // As of Dec 2019, the logrus go.mod file uses 1.13: // https://github.com/sirupsen/logrus/blob/master/go.mod go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 // v1.4.0 is required for for the log.WithContext. github.com/sirupsen/logrus v1.4.0 ) replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/nrlogrusplugin.go000066400000000000000000000133401510742411500272410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrlogrusplugin decorates logs for sending to the New Relic backend. // // Use this package if you already send your logs to New Relic and want to // enable linking between your APM events and traces with your logs. // // Since Logrus is completely api-compatible with the stdlib logger, you can // replace your `"log"` imports with `log "github.com/sirupsen/logrus"` and // follow the steps below to enable the logging product for use with the stdlib // Go logger. // // Using `logger.WithField` // (https://godoc.org/github.com/sirupsen/logrus#Logger.WithField) and // `logger.WithFields` // (https://godoc.org/github.com/sirupsen/logrus#Logger.WithFields) is // supported. However, if the field key collides with one of the keys used by // the New Relic Formatter, the value will be overwritten. Reserved keys are // those found in the `logcontext` package // (https://godoc.org/github.com/newrelic/go-agent/v3/integrations/logcontext/#pkg-constants). // // Supported types for `logger.WithField` and `logger.WithFields` field values // are numbers, booleans, strings, and errors. Func types are dropped and all // other types are converted to strings. // // Requires v1.4.0 of the Logrus package or newer. // // # Configuration // // For the best linking experience be sure to enable Distributed Tracing: // // app, err := newrelic.NewApplication( // newrelic.ConfigAppName("Logs in Context"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDistributedTracerEnabled(true), // ) // // To enable log decoration, set your log's formatter to the // `nrlogrusplugin.ContextFormatter` // // logger := log.New() // logger.SetFormatter(nrlogrusplugin.ContextFormatter{}) // // or if you are using the logrus standard logger // // log.SetFormatter(nrlogrusplugin.ContextFormatter{}) // // The logger will now look for a newrelic.Transaction inside its context and // decorate logs accordingly. Therefore, the Transaction must be added to the // context and passed to the logger. For example, this logging call // // logger.Info("Hello New Relic!") // // must be transformed to include the context, such as: // // ctx := newrelic.NewContext(context.Background(), txn) // logger.WithContext(ctx).Info("Hello New Relic!") // // # Troubleshooting // // When properly configured, your log statements will be in JSON format with // one message per line: // // {"message":"Hello New Relic!","log.level":"info","trace.id":"469a04f6c1278593","span.id":"9f365c71f0f04a98","entity.type":"SERVICE","entity.guid":"MTE3ODUwMHxBUE18QVBQTElDQVRJT058Mjc3MDU2Njc1","hostname":"my.hostname","timestamp":1568917432034,"entity.name":"Example Application"} // // If the `trace.id` key is missing, be sure that Distributed Tracing is // enabled and that the Transaction context has been added to the logger using // `WithContext` (https://godoc.org/github.com/sirupsen/logrus#Logger.WithContext). package nrlogrusplugin import ( "bytes" "encoding/json" "fmt" "github.com/newrelic/go-agent/v3/integrations/logcontext" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/jsonx" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func init() { internal.TrackUsage("integration", "logcontext", "logrus") } type logFields map[string]interface{} // ContextFormatter is a `logrus.Formatter` that will format logs for sending // to New Relic. type ContextFormatter struct{} // Format renders a single log entry. func (f ContextFormatter) Format(e *logrus.Entry) ([]byte, error) { // 12 = 6 from GetLinkingMetadata + 6 more below data := make(logFields, len(e.Data)+12) for k, v := range e.Data { data[k] = v } if ctx := e.Context; nil != ctx { if txn := newrelic.FromContext(ctx); nil != txn { logcontext.AddLinkingMetadata(data, txn.GetLinkingMetadata()) } } data[logcontext.KeyTimestamp] = uint64(e.Time.UnixNano()) / uint64(1000*1000) data[logcontext.KeyMessage] = e.Message data[logcontext.KeyLevel] = e.Level if e.HasCaller() { data[logcontext.KeyFile] = e.Caller.File data[logcontext.KeyLine] = e.Caller.Line data[logcontext.KeyMethod] = e.Caller.Function } var b *bytes.Buffer if e.Buffer != nil { b = e.Buffer } else { b = &bytes.Buffer{} } writeDataJSON(b, data) return b.Bytes(), nil } func writeDataJSON(buf *bytes.Buffer, data logFields) { buf.WriteByte('{') var needsComma bool for k, v := range data { if needsComma { buf.WriteByte(',') } else { needsComma = true } jsonx.AppendString(buf, k) buf.WriteByte(':') writeValue(buf, v) } buf.WriteByte('}') buf.WriteByte('\n') } func writeValue(buf *bytes.Buffer, val interface{}) { switch v := val.(type) { case string: jsonx.AppendString(buf, v) case bool: if v { buf.WriteString("true") } else { buf.WriteString("false") } case uint8: jsonx.AppendInt(buf, int64(v)) case uint16: jsonx.AppendInt(buf, int64(v)) case uint32: jsonx.AppendInt(buf, int64(v)) case uint64: jsonx.AppendInt(buf, int64(v)) case uint: jsonx.AppendInt(buf, int64(v)) case uintptr: jsonx.AppendInt(buf, int64(v)) case int8: jsonx.AppendInt(buf, int64(v)) case int16: jsonx.AppendInt(buf, int64(v)) case int32: jsonx.AppendInt(buf, int64(v)) case int: jsonx.AppendInt(buf, int64(v)) case int64: jsonx.AppendInt(buf, v) case float32: jsonx.AppendFloat(buf, float64(v)) case float64: jsonx.AppendFloat(buf, v) case logrus.Level: jsonx.AppendString(buf, v.String()) case error: jsonx.AppendString(buf, v.Error()) default: if m, ok := v.(json.Marshaler); ok { if js, err := m.MarshalJSON(); nil == err { buf.Write(js) return } } jsonx.AppendString(buf, fmt.Sprintf("%#v", v)) } } go-agent-3.42.0/v3/integrations/logcontext/nrlogrusplugin/nrlogrusplugin_test.go000066400000000000000000000266741510742411500303160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlogrusplugin import ( "bytes" "context" "encoding/json" "errors" "io" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/sysinfo" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/sirupsen/logrus" ) var ( testTime = time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) matchAnything = struct{}{} host, _ = sysinfo.Hostname() ) func newTestLogger(out io.Writer) *logrus.Logger { l := logrus.New() l.Formatter = ContextFormatter{} l.SetReportCaller(true) l.SetOutput(out) return l } func validateOutput(t *testing.T, out *bytes.Buffer, expected map[string]interface{}) { var actual map[string]interface{} if err := json.Unmarshal(out.Bytes(), &actual); nil != err { t.Fatal("failed to unmarshal log output:", err) } for k, v := range expected { found, ok := actual[k] if !ok { t.Errorf("key %s not found:\nactual=%s", k, actual) } if v != matchAnything && found != v { t.Errorf("value for key %s is incorrect:\nactual=%s\nexpected=%s", k, found, v) } } for k, v := range actual { if _, ok := expected[k]; !ok { t.Errorf("unexpected key found:\nkey=%s\nvalue=%s", k, v) } } } func BenchmarkWithOutTransaction(b *testing.B) { log := newTestLogger(bytes.NewBuffer([]byte(""))) ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func BenchmarkJSONFormatter(b *testing.B) { log := newTestLogger(bytes.NewBuffer([]byte(""))) log.Formatter = new(logrus.JSONFormatter) ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func BenchmarkTextFormatter(b *testing.B) { log := newTestLogger(bytes.NewBuffer([]byte(""))) log.Formatter = new(logrus.TextFormatter) ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func BenchmarkWithTransaction(b *testing.B) { app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("TestLogDistributedTracingDisabled") log := newTestLogger(bytes.NewBuffer([]byte(""))) ctx := newrelic.NewContext(context.Background(), txn) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("Hello World!") } } func TestLogNoContext(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) log.WithTime(testTime).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "file.name": matchAnything, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), }) } func TestLogNoTxn(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) log.WithTime(testTime).WithContext(context.Background()).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "file.name": matchAnything, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), }) } func TestLogDistributedTracingDisabled(t *testing.T) { app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("TestLogDistributedTracingDisabled") out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) ctx := newrelic.NewContext(context.Background(), txn) log.WithTime(testTime).WithContext(ctx).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), "trace.id": matchAnything, }) } func TestLogSampledFalse(t *testing.T) { app := integrationsupport.NewTestApp( func(reply *internal.ConnectReply) { reply.SetSampleNothing() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) }, func(cfg *newrelic.Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false }) txn := app.StartTransaction("TestLogSampledFalse") out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) ctx := newrelic.NewContext(context.Background(), txn) log.WithTime(testTime).WithContext(ctx).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), "trace.id": "1ae969564b34a33ecd1af05fe6923d6d", }) } func TestLogSampledTrue(t *testing.T) { app := integrationsupport.NewTestApp( func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) }, func(cfg *newrelic.Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false }) txn := app.StartTransaction("TestLogSampledTrue") out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) ctx := newrelic.NewContext(context.Background(), txn) log.WithTime(testTime).WithContext(ctx).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "span.id": "e71870997d57214c", "timestamp": float64(1417136460000), "trace.id": "1ae969564b34a33ecd1af05fe6923d6d", }) } func TestEntryUsedTwice(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) entry := log.WithTime(testTime) // First log has dt enabled, ensure trace.id and span.id are included app := integrationsupport.NewTestApp( func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) }, func(cfg *newrelic.Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false }) txn := app.StartTransaction("TestEntryUsedTwice1") ctx := newrelic.NewContext(context.Background(), txn) entry.WithContext(ctx).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "span.id": "e71870997d57214c", "timestamp": float64(1417136460000), "trace.id": "1ae969564b34a33ecd1af05fe6923d6d", }) // First log has dt enabled, ensure trace.id and span.id are included out.Reset() app = integrationsupport.NewTestApp(nil, func(cfg *newrelic.Config) { cfg.DistributedTracer.Enabled = false }) txn = app.StartTransaction("TestEntryUsedTwice2") ctx = newrelic.NewContext(context.Background(), txn) entry.WithContext(ctx).Info("Hello World! Again!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World! Again!", "method.name": matchAnything, "timestamp": float64(1417136460000), }) } func TestEntryError(t *testing.T) { app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("TestEntryError") out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) ctx := newrelic.NewContext(context.Background(), txn) log.WithTime(testTime).WithContext(ctx).WithField("func", func() {}).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", // Since the err field on the Entry is private we cannot record it. // "logrus_error": `can not add field "func"`, "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), "trace.id": matchAnything, }) } func TestWithCustomField(t *testing.T) { app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("TestWithCustomField") out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) ctx := newrelic.NewContext(context.Background(), txn) log.WithTime(testTime).WithContext(ctx).WithField("zip", "zap").Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "entity.name": integrationsupport.SampleAppName, "entity.type": "SERVICE", "file.name": matchAnything, "hostname": host, "line.number": matchAnything, "log.level": "info", "message": "Hello World!", "method.name": matchAnything, "timestamp": float64(1417136460000), "zip": "zap", "trace.id": matchAnything, }) } func TestCustomFieldTypes(t *testing.T) { out := bytes.NewBuffer([]byte{}) testcases := []struct { input interface{} output string }{ {input: true, output: "true"}, {input: false, output: "false"}, {input: uint8(42), output: "42"}, {input: uint16(42), output: "42"}, {input: uint32(42), output: "42"}, {input: uint(42), output: "42"}, {input: uintptr(42), output: "42"}, {input: int8(42), output: "42"}, {input: int16(42), output: "42"}, {input: int32(42), output: "42"}, {input: int64(42), output: "42"}, {input: float32(42), output: "42"}, {input: float64(42), output: "42"}, {input: errors.New("Ooops an error"), output: `"Ooops an error"`}, {input: []int{1, 2, 3}, output: `"[]int{1, 2, 3}"`}, } for _, test := range testcases { out.Reset() writeValue(out, test.input) if out.String() != test.output { t.Errorf("Incorrect output written:\nactual=%s\nexpected=%s", out.String(), test.output) } } } func TestUnsetCaller(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) log.SetReportCaller(false) log.WithTime(testTime).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "log.level": "info", "message": "Hello World!", "timestamp": float64(1417136460000), }) } func TestCustomFieldNameCollision(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) log.SetReportCaller(false) log.WithTime(testTime).WithField("timestamp", "Yesterday").Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "log.level": "info", "message": "Hello World!", // Reserved keys will be overwritten "timestamp": float64(1417136460000), }) } type gopher struct { name string } func (g *gopher) MarshalJSON() ([]byte, error) { return json.Marshal(g.name) } func TestCustomJSONMarshaller(t *testing.T) { out := bytes.NewBuffer([]byte{}) log := newTestLogger(out) log.SetReportCaller(false) log.WithTime(testTime).WithField("gopher", &gopher{name: "sam"}).Info("Hello World!") validateOutput(t, out, map[string]interface{}{ "gopher": "sam", "log.level": "info", "message": "Hello World!", "timestamp": float64(1417136460000), }) } go-agent-3.42.0/v3/integrations/nramqp/000077500000000000000000000000001510742411500176355ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nramqp/LICENSE.txt000066400000000000000000000264501510742411500214670ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nramqp/examples/000077500000000000000000000000001510742411500214535ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nramqp/examples/consumer/000077500000000000000000000000001510742411500233065ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nramqp/examples/consumer/main.go000066400000000000000000000031401510742411500245570ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nramqp" "github.com/newrelic/go-agent/v3/newrelic" amqp "github.com/rabbitmq/amqp091-go" ) func failOnError(err error, msg string) { if err != nil { panic(fmt.Sprintf("%s: %s\n", msg, err)) } } // a rabit mq server must be running on localhost on port 5672 func main() { nrApp, err := newrelic.NewApplication( newrelic.ConfigAppName("AMQP Consumer Example App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigInfoLogger(os.Stdout), ) if err != nil { panic(err) } nrApp.WaitForConnection(time.Second * 5) amqpURL := "amqp://guest:guest@localhost:5672/" conn, err := amqp.Dial(amqpURL) failOnError(err, "Failed to connect to RabbitMQ") defer conn.Close() ch, err := conn.Channel() failOnError(err, "Failed to open a channel") defer ch.Close() q, err := ch.QueueDeclare( "hello", // name false, // durable false, // delete when unused false, // exclusive false, // no-wait nil, // arguments ) failOnError(err, "Failed to declare a queue") var forever chan struct{} handleDelivery, msgs, err := nramqp.Consume(nrApp, ch, q.Name, "", true, // auto-ack false, // exclusive false, // no-local false, // no-wait nil, // args) ) failOnError(err, "Failed to register a consumer") go func() { for d := range msgs { txn := handleDelivery(d) log.Printf("Received a message: %s\n", d.Body) txn.End() } }() log.Printf(" [*] Waiting for messages. To exit press CTRL+C") <-forever nrApp.Shutdown(time.Second * 10) } go-agent-3.42.0/v3/integrations/nramqp/examples/publisher/000077500000000000000000000000001510742411500234505ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nramqp/examples/publisher/main.go000066400000000000000000000052131510742411500247240ustar00rootroot00000000000000package main import ( "fmt" "io" "net/http" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nramqp" "github.com/newrelic/go-agent/v3/newrelic" amqp "github.com/rabbitmq/amqp091-go" ) var indexHTML = `

Send a Rabbit MQ Message



` func failOnError(err error, msg string) { if err != nil { panic(fmt.Sprintf("%s: %s\n", msg, err)) } } type amqpServer struct { ch *amqp.Channel exchange string routingKey string url string } func NewServer(channel *amqp.Channel, exchangeName, routingKeyName string, url string) *amqpServer { return &amqpServer{ channel, exchangeName, routingKeyName, url, } } func (serv *amqpServer) index(w http.ResponseWriter, r *http.Request) { io.WriteString(w, indexHTML) } func (serv *amqpServer) publishPlainTxtMessage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // get the message from the HTTP form r.ParseForm() message := r.Form.Get("msg") err := nramqp.PublishWithContext(serv.ch, ctx, serv.exchange, // exchange serv.routingKey, // routing key serv.url, // url false, // mandatory false, // immediate amqp.Publishing{ ContentType: "text/plain", Body: []byte(message), }) if err != nil { txn := newrelic.FromContext(ctx) txn.NoticeError(err) } serv.index(w, r) } // a rabit mq server must be running on localhost on port 5672 func main() { nrApp, err := newrelic.NewApplication( newrelic.ConfigAppName("AMQP Publisher Example App"), newrelic.ConfigFromEnvironment(), newrelic.ConfigInfoLogger(os.Stdout), ) if err != nil { panic(err) } nrApp.WaitForConnection(time.Second * 5) amqpURL := "amqp://guest:guest@localhost:5672/" conn, err := amqp.Dial(amqpURL) failOnError(err, "Failed to connect to RabbitMQ") defer conn.Close() ch, err := conn.Channel() failOnError(err, "Failed to open a channel") defer ch.Close() q, err := ch.QueueDeclare( "hello", // name false, // durable false, // delete when unused false, // exclusive false, // no-wait nil, // arguments ) failOnError(err, "Failed to declare a queue") server := NewServer(ch, "", q.Name, amqpURL) http.HandleFunc(newrelic.WrapHandleFunc(nrApp, "/", server.index)) http.HandleFunc(newrelic.WrapHandleFunc(nrApp, "/message", server.publishPlainTxtMessage)) fmt.Println("\n\nlistening on: http://localhost:8000/") http.ListenAndServe(":8000", nil) nrApp.Shutdown(time.Second * 10) } go-agent-3.42.0/v3/integrations/nramqp/go.mod000066400000000000000000000003221510742411500207400ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nramqp go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/rabbitmq/amqp091-go v1.9.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nramqp/headers.go000066400000000000000000000030751510742411500216040ustar00rootroot00000000000000package nramqp import ( "encoding/json" "fmt" "net/http" "github.com/newrelic/go-agent/v3/newrelic" amqp "github.com/rabbitmq/amqp091-go" ) const ( MaxHeaderLen = 4096 ) // Adds Distributed Tracing headers to the amqp table object func injectDtHeaders(txn *newrelic.Transaction, headers amqp.Table) amqp.Table { dummyHeaders := http.Header{} txn.InsertDistributedTraceHeaders(dummyHeaders) if headers == nil { headers = amqp.Table{} } dtHeaders := dummyHeaders.Get(newrelic.DistributedTraceNewRelicHeader) if dtHeaders != "" { headers[newrelic.DistributedTraceNewRelicHeader] = dtHeaders } traceParent := dummyHeaders.Get(newrelic.DistributedTraceW3CTraceParentHeader) if traceParent != "" { headers[newrelic.DistributedTraceW3CTraceParentHeader] = traceParent } traceState := dummyHeaders.Get(newrelic.DistributedTraceW3CTraceStateHeader) if traceState != "" { headers[newrelic.DistributedTraceW3CTraceStateHeader] = traceState } return headers } func toHeader(headers amqp.Table) http.Header { headersHTTP := http.Header{} if headers == nil { return headersHTTP } for k, v := range headers { headersHTTP.Set(k, fmt.Sprintf("%v", v)) } return headersHTTP } func getHeadersAttributeString(hdrs amqp.Table) (string, error) { if len(hdrs) == 0 { return "", nil } delete(hdrs, newrelic.DistributedTraceNewRelicHeader) delete(hdrs, newrelic.DistributedTraceW3CTraceParentHeader) delete(hdrs, newrelic.DistributedTraceW3CTraceStateHeader) if len(hdrs) == 0 { return "", nil } bytes, err := json.Marshal(hdrs) return string(bytes), err } go-agent-3.42.0/v3/integrations/nramqp/headers_test.go000066400000000000000000000151501510742411500226400ustar00rootroot00000000000000package nramqp import ( "encoding/json" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" amqp "github.com/rabbitmq/amqp091-go" ) var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.AccountID = "123" reply.TrustedAccountKey = "123" reply.PrimaryAppID = "456" } var cfgFn = func(cfg *newrelic.Config) { cfg.Attributes.Include = append(cfg.Attributes.Include, newrelic.AttributeMessageRoutingKey, newrelic.AttributeMessageQueueName, newrelic.AttributeMessageExchangeType, newrelic.AttributeMessageReplyTo, newrelic.AttributeMessageCorrelationID, newrelic.AttributeMessageHeaders, ) } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, cfgFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } func TestAddHeaderAttribute(t *testing.T) { app := createTestApp() txn := app.StartTransaction("test") hdrs := amqp.Table{ "str": "hello", "int": 5, "bool": true, "nil": nil, "time": time.Now(), "bytes": []byte("a slice of bytes"), "decimal": amqp.Decimal{Scale: 2, Value: 12345}, "zero decimal": amqp.Decimal{Scale: 0, Value: 12345}, } attrStr, err := getHeadersAttributeString(hdrs) if err != nil { t.Fatal(err) } integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageHeaders, attrStr, hdrs) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{ { AgentAttributes: map[string]interface{}{ newrelic.AttributeMessageHeaders: attrStr, }, }, }) } func TestInjectHeaders(t *testing.T) { nrApp := createTestApp() txn := nrApp.StartTransaction("test txn") defer txn.End() msg := amqp.Publishing{} msg.Headers = injectDtHeaders(txn, msg.Headers) if len(msg.Headers) != 3 { t.Error("Expected DT headers to be injected into Headers object") } } func TestInjectHeadersPreservesExistingHeaders(t *testing.T) { nrApp := createTestApp() txn := nrApp.StartTransaction("test txn") defer txn.End() msg := amqp.Publishing{ Headers: amqp.Table{ "one": 1, "two": 2, }, } msg.Headers = injectDtHeaders(txn, msg.Headers) if len(msg.Headers) != 5 { t.Error("Expected DT headers to be injected into Headers object") } } func TestToHeader(t *testing.T) { nrApp := createTestApp() txn := nrApp.StartTransaction("test txn") defer txn.End() msg := amqp.Publishing{ Headers: amqp.Table{ "one": 1, "two": 2, }, } msg.Headers = injectDtHeaders(txn, msg.Headers) hdr := toHeader(msg.Headers) if v := hdr.Get(newrelic.DistributedTraceNewRelicHeader); v == "" { t.Errorf("header did not contain a DT header with the key %s", newrelic.DistributedTraceNewRelicHeader) } if v := hdr.Get(newrelic.DistributedTraceW3CTraceParentHeader); v == "" { t.Errorf("header did not contain a DT header with the key %s", newrelic.DistributedTraceW3CTraceParentHeader) } if v := hdr.Get(newrelic.DistributedTraceW3CTraceStateHeader); v == "" { t.Errorf("header did not contain a DT header with the key %s", newrelic.DistributedTraceW3CTraceStateHeader) } } func BenchmarkGetAttributeHeaders(b *testing.B) { hdrs := amqp.Table{ "str": "hello", "int": 5, "bool": true, "nil": nil, "time": time.Now(), "bytes": []byte("a slice of bytes"), "decimal": amqp.Decimal{Scale: 2, Value: 12345}, "zero decimal": amqp.Decimal{Scale: 0, Value: 12345}, } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { getHeadersAttributeString(hdrs) } } func TestGetAttributeHeaders(t *testing.T) { ti := time.Now() hdrs := amqp.Table{ "str": "hello", "int": 5, "bool": true, "nil": nil, "time": ti, "bytes": []byte("a slice of bytes"), "decimal": amqp.Decimal{Scale: 2, Value: 12345}, "zero decimal": amqp.Decimal{Scale: 0, Value: 12345}, "array": []interface{}{5, true, "hi", ti}, } hdrStr, err := getHeadersAttributeString(hdrs) if err != nil { t.Fatal(err) } t.Log(hdrStr) var v map[string]any err = json.Unmarshal([]byte(hdrStr), &v) if err != nil { t.Fatal(err) } if len(v) != 9 { t.Errorf("expected 6 key value pairs, but got %d", len(v)) } _, ok := v["str"] if !ok { t.Error("string header key value pair was dropped") } _, ok = v["bytes"] if !ok { t.Error("bytes header key value pair was dropped") } _, ok = v["int"] if !ok { t.Error("int header key value pair was dropped") } _, ok = v["bool"] if !ok { t.Error("bool header key value pair was dropped") } _, ok = v["nil"] if !ok { t.Error("nil header key value pair was dropped") } _, ok = v["decimal"] if !ok { t.Error("decimal header key value pair was dropped") } _, ok = v["zero decimal"] if !ok { t.Error("zero decimal header key value pair was dropped") } _, ok = v["array"] if !ok { t.Error("array header key value pair was dropped") } _, ok = v["time"] if !ok { t.Error("time header key value pair was dropped") } } func TestGetAttributeHeadersEmpty(t *testing.T) { hdrs := amqp.Table{} hdrStr, err := getHeadersAttributeString(hdrs) if err != nil { t.Fatal(err) } if hdrStr != "" { t.Errorf("should return empty string for empty or nil header table, instead got: %s", hdrStr) } } func TestGetAttributeHeadersNil(t *testing.T) { hdrStr, err := getHeadersAttributeString(nil) if err != nil { t.Fatal(err) } if hdrStr != "" { t.Errorf("should return empty string for empty or nil header table, instead got: %s", hdrStr) } } func TestGetAttributeHeadersIgnoresDT(t *testing.T) { app := createTestApp() txn := app.StartTransaction("test") defer txn.End() hdrs := amqp.Table{ "str": "hello", } injectDtHeaders(txn, hdrs) hdrStr, err := getHeadersAttributeString(hdrs) if err != nil { t.Fatal(err) } t.Log(hdrStr) var v map[string]any err = json.Unmarshal([]byte(hdrStr), &v) if err != nil { t.Fatal(err) } if len(v) != 1 { t.Errorf("expected 1 key value pair, but got %d", len(v)) } val, ok := v["str"] if !ok { t.Error("string header key value pair was dropped") } else if val.(string) != "hello" { t.Error("string header value was corrupted") } } func TestGetAttributeHeadersEmptyAfterStrippingDT(t *testing.T) { app := createTestApp() txn := app.StartTransaction("test") defer txn.End() hdrs := amqp.Table{} injectDtHeaders(txn, hdrs) hdrStr, err := getHeadersAttributeString(hdrs) if err != nil { t.Fatal(err) } if hdrStr != "" { t.Errorf("expected an empty header string, but got: %s", hdrStr) } } go-agent-3.42.0/v3/integrations/nramqp/nramqp.go000066400000000000000000000122721510742411500214660ustar00rootroot00000000000000package nramqp import ( "context" "strings" amqp "github.com/rabbitmq/amqp091-go" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) const ( RabbitMQLibrary = "RabbitMQ" ) func init() { internal.TrackUsage("integration", "messagebroker", "nramqp") } func createProducerSegment(exchange, key string) *newrelic.MessageProducerSegment { s := newrelic.MessageProducerSegment{ Library: RabbitMQLibrary, DestinationName: "Default", DestinationType: newrelic.MessageQueue, } if exchange != "" { s.DestinationName = exchange s.DestinationType = newrelic.MessageExchange } else if key != "" { s.DestinationName = key } return &s } func GetHostAndPortFromURL(url string) (string, string) { // url is of format amqp://user:password@host:port or amqp://host:port var hostPortPart string // extract the part after "@" symbol, if present if parts := strings.Split(url, "@"); len(parts) == 2 { hostPortPart = parts[1] } else { // assume the whole url after "amqp://" is the host:port part hostPortPart = strings.TrimPrefix(url, "amqp://") } // split the host:port part strippedURL := strings.Split(hostPortPart, ":") if len(strippedURL) != 2 { return "", "" } return strippedURL[0], strippedURL[1] } // PublishedWithContext looks for a newrelic transaction in the context object, and if found, creates a message producer segment. // It will also inject distributed tracing headers into the message. func PublishWithContext(ch *amqp.Channel, ctx context.Context, exchange, key, url string, mandatory, immediate bool, msg amqp.Publishing) error { host, port := GetHostAndPortFromURL(url) txn := newrelic.FromContext(ctx) if txn != nil { // generate message broker segment s := createProducerSegment(exchange, key) // capture telemetry for AMQP producer if msg.Headers != nil && len(msg.Headers) > 0 { hdrStr, err := getHeadersAttributeString(msg.Headers) if err != nil { return err } integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageHeaders, hdrStr) } s.StartTime = txn.StartSegmentNow() // inject DT headers into headers object msg.Headers = injectDtHeaders(txn, msg.Headers) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeSpanKind, "producer") integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeServerAddress, host) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeServerPort, port) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageDestinationName, exchange) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageRoutingKey, key) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageCorrelationID, msg.CorrelationId) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageReplyTo, msg.ReplyTo) err := ch.PublishWithContext(ctx, exchange, key, mandatory, immediate, msg) s.End() return err } else { return ch.PublishWithContext(ctx, exchange, key, mandatory, immediate, msg) } } // Consume performs a consume request on the provided amqp Channel, and returns a consume function, a consumer channel, and an error. // The consumer function should be applied to each amqp Delivery that is read from the consume Channel, in order to collect tracing data // on that message. The consume function will then return a transaction for that message. func Consume(app *newrelic.Application, ch *amqp.Channel, queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (func(amqp.Delivery) *newrelic.Transaction, <-chan amqp.Delivery, error) { var handler func(amqp.Delivery) *newrelic.Transaction if app != nil { handler = func(delivery amqp.Delivery) *newrelic.Transaction { namer := internal.MessageMetricKey{ Library: RabbitMQLibrary, DestinationType: string(newrelic.MessageExchange), DestinationName: queue, Consumer: true, } txn := app.StartTransaction(namer.Name()) hdrs := toHeader(delivery.Headers) txn.AcceptDistributedTraceHeaders(newrelic.TransportAMQP, hdrs) if delivery.Headers != nil && len(delivery.Headers) > 0 { hdrStr, err := getHeadersAttributeString(delivery.Headers) if err == nil { integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageHeaders, hdrStr, nil) } } integrationsupport.AddAgentAttribute(txn, newrelic.AttributeSpanKind, "consumer", nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageQueueName, queue, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageDestinationName, queue, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessagingDestinationPublishName, delivery.Exchange, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageRoutingKey, delivery.RoutingKey, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageCorrelationID, delivery.CorrelationId, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageReplyTo, delivery.ReplyTo, nil) return txn } } msgChan, err := ch.Consume(queue, consumer, autoAck, exclusive, noLocal, noWait, args) return handler, msgChan, err } go-agent-3.42.0/v3/integrations/nramqp/nramqp_test.go000066400000000000000000000046121510742411500225240ustar00rootroot00000000000000package nramqp import ( "testing" "github.com/newrelic/go-agent/v3/newrelic" ) func BenchmarkCreateProducerSegment(b *testing.B) { app := createTestApp() txn := app.StartTransaction("test") defer txn.End() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { createProducerSegment("exchange", "key") } } func TestCreateProducerSegment(t *testing.T) { app := createTestApp() txn := app.StartTransaction("test") defer txn.End() type testObject struct { exchange string key string expect newrelic.MessageProducerSegment } tests := []testObject{ { "test exchange", "", newrelic.MessageProducerSegment{ DestinationName: "test exchange", DestinationType: newrelic.MessageExchange, }, }, { "", "test queue", newrelic.MessageProducerSegment{ DestinationName: "test queue", DestinationType: newrelic.MessageQueue, }, }, { "", "", newrelic.MessageProducerSegment{ DestinationName: "Default", DestinationType: newrelic.MessageQueue, }, }, { "test exchange", "test queue", newrelic.MessageProducerSegment{ DestinationName: "test exchange", DestinationType: newrelic.MessageExchange, }, }, } for _, test := range tests { s := createProducerSegment(test.exchange, test.key) if s.DestinationName != test.expect.DestinationName { t.Errorf("expected destination name %s, got %s", test.expect.DestinationName, s.DestinationName) } if s.DestinationType != test.expect.DestinationType { t.Errorf("expected destination type %s, got %s", test.expect.DestinationType, s.DestinationType) } } } func TestHostAndPortParsing(t *testing.T) { app := createTestApp() txn := app.StartTransaction("test") defer txn.End() type testObject struct { url string expectHost string expectPort string } tests := []testObject{ { "amqp://user:password@host:port", "host", "port", }, { "amqp://host:port", "host", "port", }, { "aaa://host:port", "", "", }, { "amqp://user:password@host", "", "", }, { "amqp://user:password@host:port:extra", "", "", }, } for _, test := range tests { host, port := GetHostAndPortFromURL(test.url) if host != test.expectHost { t.Errorf("expected host %s, got %s", test.expectHost, host) } if port != test.expectPort { t.Errorf("expected port %s, got %s", test.expectPort, port) } } } go-agent-3.42.0/v3/integrations/nrawsbedrock/000077500000000000000000000000001510742411500210235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrawsbedrock/LICENSE.txt000066400000000000000000000264501510742411500226550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrawsbedrock/README.md000066400000000000000000000013141510742411500223010ustar00rootroot00000000000000# v3/integrations/nrawsbedrock [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawsbedrock?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawsbedrock) Package `nrawsbedrock` instruments https://github.com/aws/aws-sdk-go-v2/service/bedrockruntime requests. This integration works independently of the `nrawssdk-v2` integration, which instruments AWS middleware components generally, while this one instruments Bedrock AI model invocations specifically and in detail. ```go import "github.com/newrelic/go-agent/v3/integrations/nrawsbedrock" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawsbedrock). go-agent-3.42.0/v3/integrations/nrawsbedrock/example/000077500000000000000000000000001510742411500224565ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrawsbedrock/example/main.go000066400000000000000000000167371510742411500237470ustar00rootroot00000000000000// Example Bedrock client application with New Relic instrumentation package main import ( "context" "fmt" "os" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/bedrock" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/newrelic/go-agent/v3/integrations/nrawsbedrock" "github.com/newrelic/go-agent/v3/newrelic" ) const region = "us-east-1" func main() { sdkConfig, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) if err != nil { panic(err) } // Create a New Relic application. This will look for your license key in an // environment variable called NEW_RELIC_LICENSE_KEY. This example turns on // Distributed Tracing, but that's not required. app, err := newrelic.NewApplication( newrelic.ConfigFromEnvironment(), newrelic.ConfigAppName("Example Bedrock App"), newrelic.ConfigDebugLogger(os.Stdout), //newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), newrelic.ConfigAIMonitoringEnabled(true), newrelic.ConfigAIMonitoringRecordContentEnabled(true), ) if nil != err { fmt.Println(err) os.Exit(1) } // For demo purposes only. Don't use the app.WaitForConnection call in // production unless this is a very short-lived process and the caller // doesn't block or exit if there's an error. app.WaitForConnection(5 * time.Second) listModels(sdkConfig) brc := bedrockruntime.NewFromConfig(sdkConfig) simpleEmbedding(app, brc) simpleChatCompletionError(app, brc) simpleChatCompletion(app, brc) processedChatCompletionStream(app, brc) manualChatCompletionStream(app, brc) app.Shutdown(10 * time.Second) } func listModels(sdkConfig aws.Config) { fmt.Println("================================================== MODELS") bedrockClient := bedrock.NewFromConfig(sdkConfig) result, err := bedrockClient.ListFoundationModels(context.TODO(), &bedrock.ListFoundationModelsInput{}) if err != nil { panic(err) } if len(result.ModelSummaries) == 0 { fmt.Println("no models found") } for _, modelSummary := range result.ModelSummaries { fmt.Printf("Name: %-30s | Provider: %-20s | ID: %s\n", *modelSummary.ModelName, *modelSummary.ProviderName, *modelSummary.ModelId) } } func simpleChatCompletionError(app *newrelic.Application, brc *bedrockruntime.Client) { fmt.Println("================================================== CHAT COMPLETION WITH ERROR") // Start recording a New Relic transaction txn := app.StartTransaction("demo-chat-completion-error") contentType := "application/json" model := "amazon.titan-text-lite-v1" // // without nrawsbedrock instrumentation, the call to invoke the model would be: // output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{ // ... // }) // _, err := nrawsbedrock.InvokeModel(app, brc, newrelic.NewContext(context.Background(), txn), &bedrockruntime.InvokeModelInput{ ContentType: &contentType, Accept: &contentType, Body: []byte(`{ "inputTexxt": "What is your quest?", "textGenerationConfig": { "temperature": 0.5, "maxTokenCount": 100, "stopSequences": [], "topP": 1 } }`), ModelId: &model, }) txn.End() if err != nil { fmt.Printf("error: %v\n", err) } } func simpleEmbedding(app *newrelic.Application, brc *bedrockruntime.Client) { fmt.Println("================================================== EMBEDDING") // Start recording a New Relic transaction contentType := "application/json" model := "amazon.titan-embed-text-v1" // // without nrawsbedrock instrumentation, the call to invoke the model would be: // output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{ // ... // }) // output, err := nrawsbedrock.InvokeModel(app, brc, context.Background(), &bedrockruntime.InvokeModelInput{ ContentType: &contentType, Accept: &contentType, Body: []byte(`{ "inputText": "What is your quest?" }`), ModelId: &model, }) if err != nil { fmt.Printf("error: %v\n", err) } if output != nil { fmt.Printf("Result: %v\n", string(output.Body)) } } func simpleChatCompletion(app *newrelic.Application, brc *bedrockruntime.Client) { fmt.Println("================================================== COMPLETION") // Start recording a New Relic transaction txn := app.StartTransaction("demo-chat-completion") contentType := "application/json" model := "amazon.titan-text-lite-v1" // // without nrawsbedrock instrumentation, the call to invoke the model would be: // output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{ // ... // }) // app.SetLLMTokenCountCallback(func(model, data string) int { return 42 }) output, err := nrawsbedrock.InvokeModel(app, brc, newrelic.NewContext(context.Background(), txn), &bedrockruntime.InvokeModelInput{ ContentType: &contentType, Accept: &contentType, Body: []byte(`{ "inputText": "What is your quest?", "textGenerationConfig": { "temperature": 0.5, "maxTokenCount": 100, "stopSequences": [], "topP": 1 } }`), ModelId: &model, }) txn.End() app.SetLLMTokenCountCallback(nil) if err != nil { fmt.Printf("error: %v\n", err) } if output != nil { fmt.Printf("Result: %v\n", string(output.Body)) } } // This example shows a stream invocation where we let the nrawsbedrock integration retrieve // all the stream output for us. func processedChatCompletionStream(app *newrelic.Application, brc *bedrockruntime.Client) { fmt.Println("================================================== STREAM (PROCESSED)") contentType := "application/json" model := "anthropic.claude-v2" err := nrawsbedrock.ProcessModelWithResponseStreamAttributes(app, brc, context.Background(), func(data []byte) error { fmt.Printf(">>> Received %s\n", string(data)) return nil }, &bedrockruntime.InvokeModelWithResponseStreamInput{ ModelId: &model, ContentType: &contentType, Accept: &contentType, Body: []byte(`{ "prompt": "Human: Tell me a story.\n\nAssistant:", "max_tokens_to_sample": 200, "temperature": 0.5 }`), }, map[string]any{ "llm.what_is_this": "processed stream invocation", }) if err != nil { fmt.Printf("ERROR processing model: %v\n", err) } } // This example shows a stream invocation where we manually process the retrieval // of the stream output. func manualChatCompletionStream(app *newrelic.Application, brc *bedrockruntime.Client) { fmt.Println("================================================== STREAM (MANUAL)") contentType := "application/json" model := "anthropic.claude-v2" output, err := nrawsbedrock.InvokeModelWithResponseStreamAttributes(app, brc, context.Background(), &bedrockruntime.InvokeModelWithResponseStreamInput{ ModelId: &model, ContentType: &contentType, Accept: &contentType, Body: []byte(`{ "prompt": "Human: Tell me a story.\n\nAssistant:", "max_tokens_to_sample": 200, "temperature": 0.5 }`)}, map[string]any{ "llm.what_is_this": "manual chat completion stream", }, ) if err != nil { fmt.Printf("ERROR processing model: %v\n", err) return } stream := output.Response.GetStream() for event := range stream.Events() { switch v := event.(type) { case *types.ResponseStreamMemberChunk: fmt.Println("=====[event received]=====") fmt.Println(string(v.Value.Bytes)) output.RecordEvent(v.Value.Bytes) default: fmt.Println("=====[unknown value received]=====") } } output.Close() stream.Close() } go-agent-3.42.0/v3/integrations/nrawsbedrock/go.mod000066400000000000000000000006261510742411500221350ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrawsbedrock go 1.24 require ( github.com/aws/aws-sdk-go-v2 v1.26.0 github.com/aws/aws-sdk-go-v2/config v1.27.4 github.com/aws/aws-sdk-go-v2/service/bedrock v1.7.3 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.1 github.com/google/uuid v1.6.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrawsbedrock/nrawsbedrock.go000066400000000000000000001065531510742411500240500ustar00rootroot00000000000000// Copyright New Relic, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package nrawsbedrock instruments AI model invocation requests made by the // https://github.com/aws/aws-sdk-go-v2/service/bedrockruntime library. // // Specifically, this provides instrumentation for the InvokeModel and InvokeModelWithResponseStream // bedrock client API library functions. // // To use this integration, enable the New Relic AIMonitoring configuration options // in your application, import this integration, and use the model invocation calls // from this library in place of the corresponding ones from the AWS Bedrock // runtime library, as documented below. // // The relevant configuration options are passed to the NewApplication function and include // // ConfigAIMonitoringEnabled(true), // enable (or disable if false) this integration // ConfigAIMonitoringStreamingEnabled(true), // enable instrumentation of streaming invocations // ConfigAIMonitoringRecordContentEnabled(true), // include input/output data in instrumentation // // Currently, the following must also be set for AIM reporting to function correctly: // // ConfigCustomInsightsEventsEnabled(true) // (the default) // ConfigHighSecurityEnabled(false) // (the default) // // Or, if ConfigFromEnvironment() is included in your configuration options, the above configuration // options may be specified using these environment variables, respectively: // // NEW_RELIC_AI_MONITORING_ENABLED=true // NEW_RELIC_AI_MONITORING_STREAMING_ENABLED=true // NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED=true // NEW_RELIC_HIGH_SECURITY=false // // The values for these variables may be any form accepted by strconv.ParseBool (e.g., 1, t, T, true, TRUE, True, // 0, f, F, false, FALSE, or False). // // See example/main.go for a working sample. package nrawsbedrock import ( "context" "encoding/json" "errors" "runtime/debug" "strings" "sync" "time" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/google/uuid" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var ( reportStreamingDisabled func() ErrMissingResponseData = errors.New("missing response data") ) func init() { reportStreamingDisabled = sync.OnceFunc(func() { internal.TrackUsage("Go", "ML", "Streaming", "Disabled") }) // Get the version of the AWS Bedrock library we're using info, ok := debug.ReadBuildInfo() if info != nil && ok { for _, module := range info.Deps { if module != nil && strings.Contains(module.Path, "/aws/aws-sdk-go-v2/service/bedrockruntime") { internal.TrackUsage("Go", "ML", "Bedrock", module.Version) return } } } internal.TrackUsage("Go", "ML", "Bedrock", "unknown") } // isEnabled determines if AI Monitoring is enabled in the app's options. // It returns true if we should proceed with instrumentation. Additionally, // it sets the Go/ML/Streaming/Disabled supportability metric if we discover // that streaming is disabled, but ONLY does so the first time we try. Since // we need to initialize the app and load options before we know if that one // gets sent, we have to wait until later on to report that. // // streaming indicates if you're asking if it's ok to instrument streaming calls. // The return values are two booleans: the first indicates if AI instrumentation // is enabled at all, the second tells if it is permitted to record request and // response data (as opposed to just metadata). func isEnabled(app *newrelic.Application, streaming bool) (bool, bool) { if app == nil { return false, false } config, _ := app.Config() if !config.AIMonitoring.Streaming.Enabled { if reportStreamingDisabled != nil { reportStreamingDisabled() } if streaming { // we asked for streaming but it's not enabled return false, false } } return config.AIMonitoring.Enabled, config.AIMonitoring.RecordContent.Enabled } // Modeler is any type that can invoke Bedrock models (e.g., bedrockruntime.Client). type Modeler interface { InvokeModel(context.Context, *bedrockruntime.InvokeModelInput, ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) InvokeModelWithResponseStream(context.Context, *bedrockruntime.InvokeModelWithResponseStreamInput, ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelWithResponseStreamOutput, error) } // ResponseStream tracks the model invocation throughout its lifetime until all stream events // are processed. type ResponseStream struct { // The request parameters that started the invocation ctx context.Context app *newrelic.Application params *bedrockruntime.InvokeModelWithResponseStreamInput attrs map[string]any meta map[string]any recordContentEnabled bool closeTxn bool txn *newrelic.Transaction seg *newrelic.Segment completionID string seq int output strings.Builder stopReason string // The model output Response *bedrockruntime.InvokeModelWithResponseStreamOutput } type modelResultList struct { output string completionReason string tokenCount int } type modelInputList struct { input string role string tokenCount int } // InvokeModelWithResponseStream invokes a model but unlike the InvokeModel method, the data returned // is a stream of multiple events instead of a single response value. // This function is the analogue of the bedrockruntime library InvokeModelWithResponseStream function, // so that, given a bedrockruntime.Client b, where you would normally call the AWS method // // response, err := b.InvokeModelWithResponseStream(c, p, f...) // // You instead invoke the New Relic InvokeModelWithResponseStream function as: // // rstream, err := nrbedrock.InvokeModelWithResponseStream(app, b, c, p, f...) // // where app is your New Relic Application value. // // If using the bedrockruntime library directly, you would then process the response stream value // (the response variable in the above example), iterating over the provided channel where the stream // data appears until it is exhausted, and then calling Close() on the stream (see the bedrock API // documentation for details). // // When using the New Relic nrawsbedrock integration, this response value is available as // rstream.Response. You would perform the same operations as you would directly with the bedrock API // once you have that value. // Since this means control has passed back to your code for processing of the stream data, you need to // add instrumentation calls to your processing code: // // rstream.RecordEvent(content) // for each event received from the stream // rstream.Close() // when you are finished and are going to close the stream // // However, see ProcessModelWithResponseStream for an easier alternative. // // Either start a transaction on your own and add it to the context c passed into this function, or // a transaction will be started for you that lasts only for the duration of the model invocation. func InvokeModelWithResponseStream(app *newrelic.Application, brc Modeler, ctx context.Context, params *bedrockruntime.InvokeModelWithResponseStreamInput, optFns ...func(*bedrockruntime.Options)) (ResponseStream, error) { return InvokeModelWithResponseStreamAttributes(app, brc, ctx, params, nil, optFns...) } // InvokeModelWithResponseStreamAttributes is identical to InvokeModelWithResponseStream except that // it adds the attrs parameter, which is a // map of strings to values of any type. This map holds any custom attributes you wish to add to the reported metrics // relating to this model invocation. // // Each key in the attrs map must begin with "llm."; if any of them do not, "llm." is automatically prepended to // the attribute key before the metrics are sent out. // // We recommend including at least "llm.conversation_id" in your attributes. func InvokeModelWithResponseStreamAttributes(app *newrelic.Application, brc Modeler, ctx context.Context, params *bedrockruntime.InvokeModelWithResponseStreamInput, attrs map[string]any, optFns ...func(*bedrockruntime.Options)) (ResponseStream, error) { var aiEnabled bool var err error resp := ResponseStream{ ctx: ctx, app: app, meta: map[string]any{}, params: params, attrs: attrs, } aiEnabled, resp.recordContentEnabled = isEnabled(app, true) if aiEnabled { resp.txn = newrelic.FromContext(ctx) if resp.txn == nil { resp.txn = app.StartTransaction("InvokeModelWithResponseStream") resp.closeTxn = true } } if resp.txn != nil { integrationsupport.AddAgentAttribute(resp.txn, "llm", "", true) if params.ModelId != nil { resp.seg = resp.txn.StartSegment("Llm/completion/Bedrock/InvokeModelWithResponseStream") } else { // we don't have a model! resp.txn = nil } } start := time.Now() resp.Response, err = brc.InvokeModelWithResponseStream(ctx, params, optFns...) duration := time.Since(start).Milliseconds() if resp.txn != nil { md := resp.txn.GetTraceMetadata() resp.completionID = uuid.New().String() resp.meta = map[string]any{ "id": resp.completionID, "span_id": md.SpanID, "trace_id": md.TraceID, "request.model": *params.ModelId, "response.model": *params.ModelId, "vendor": "bedrock", "ingest_source": "Go", "duration": duration, } if err != nil { resp.txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "BedrockError", Attributes: map[string]any{ "completion_id": resp.completionID, }, }) resp.meta["error"] = true } } return resp, nil } // RecordEvent records a single stream event as read from the data stream started by InvokeModelWithStreamResponse. func (s *ResponseStream) RecordEvent(data []byte) error { if s == nil || s.txn == nil || s.app == nil { return nil } if s.params == nil || s.params.ModelId == nil || s.meta == nil { return ErrMissingResponseData } _, outputs, _ := parseModelData(s.app, *s.params.ModelId, s.meta, s.params.Body, data, s.attrs, false) for _, msg := range outputs { s.output.WriteString(msg.output) if msg.completionReason != "" { s.stopReason = msg.completionReason } } return nil } // Close finishes up the instrumentation for a response stream. func (s *ResponseStream) Close() error { if s == nil || s.app == nil || s.txn == nil { return nil } if s.params == nil || s.params.ModelId == nil || s.meta == nil { return ErrMissingResponseData } var modelInput []byte modelOutput := s.output.String() if s.params != nil && s.params.Body != nil { modelInput = s.params.Body } inputs, _, systemMessage := parseModelData(s.app, *s.params.ModelId, s.meta, modelInput, nil, s.attrs, true) // To be more runtime efficient, we don't copy the maps or rebuild them for each kind of message. // Instead, we build one map with most of the attributes common to all messages and then adjust as needed // when reporting out each metric. otherQty := 0 if systemMessage != "" { otherQty++ } if modelOutput != "" { otherQty++ } if s.stopReason != "" { s.meta["response.choices.finish_reason"] = s.stopReason } s.meta["response.number_of_messages"] = len(inputs) + otherQty s.app.RecordCustomEvent("LlmChatCompletionSummary", s.meta) delete(s.meta, "duration") s.meta["completion_id"] = s.meta["id"] delete(s.meta, "id") if systemMessage != "" { s.meta["sequence"] = s.seq s.seq++ s.meta["role"] = "system" if s.recordContentEnabled { s.meta["content"] = systemMessage } s.app.RecordCustomEvent("LlmChatCompletionMessage", s.meta) } s.meta["role"] = "user" for _, msg := range inputs { s.meta["sequence"] = s.seq s.seq++ if msg.tokenCount > 0 { s.meta["token_count"] = msg.tokenCount } else { delete(s.meta, "token_count") } if s.recordContentEnabled { s.meta["content"] = msg.input } else { delete(s.meta, "content") } s.app.RecordCustomEvent("LlmChatCompletionMessage", s.meta) } if s.app.HasLLMTokenCountCallback() { if tc, _ := s.app.InvokeLLMTokenCountCallback(*s.params.ModelId, modelOutput); tc > 0 { s.meta["token_count"] = tc } } s.meta["role"] = "assistant" s.meta["sequence"] = s.seq s.seq++ if s.recordContentEnabled { s.meta["content"] = modelOutput } else { delete(s.meta, "content") } s.app.RecordCustomEvent("LlmChatCompletionMessage", s.meta) if s.seg != nil { s.seg.End() } if s.closeTxn { s.txn.End() } return nil } // ProcessModelWithResponseStream works just like InvokeModelWithResponseStream, except that // it handles all the stream processing automatically for you. For each event received from // the response stream, it will invoke the callback function you pass into the function call // so that your application can act on the response data. When the stream is complete, the // ProcessModelWithResponseStream call will return. // // If your callback function returns an error, the processing of the response stream will // terminate at that point. func ProcessModelWithResponseStream(app *newrelic.Application, brc Modeler, ctx context.Context, callback func([]byte) error, params *bedrockruntime.InvokeModelWithResponseStreamInput, optFns ...func(*bedrockruntime.Options)) error { return ProcessModelWithResponseStreamAttributes(app, brc, ctx, callback, params, nil, optFns...) } // ProcessModelWithResponseStreamAttributes is identical to ProcessModelWithResponseStream except that // it adds the attrs parameter, which is a // map of strings to values of any type. This map holds any custom attributes you wish to add to the reported metrics // relating to this model invocation. // // Each key in the attrs map must begin with "llm."; if any of them do not, "llm." is automatically prepended to // the attribute key before the metrics are sent out. // // We recommend including at least "llm.conversation_id" in your attributes. func ProcessModelWithResponseStreamAttributes(app *newrelic.Application, brc Modeler, ctx context.Context, callback func([]byte) error, params *bedrockruntime.InvokeModelWithResponseStreamInput, attrs map[string]any, optFns ...func(*bedrockruntime.Options)) error { var err error var userErr error response, err := InvokeModelWithResponseStreamAttributes(app, brc, ctx, params, attrs, optFns...) if err != nil { return err } if response.Response == nil { return response.Close() } stream := response.Response.GetStream() defer func() { err = stream.Close() }() for event := range stream.Events() { if v, ok := event.(*types.ResponseStreamMemberChunk); ok { if userErr = callback(v.Value.Bytes); userErr != nil { break } response.RecordEvent(v.Value.Bytes) } } err = response.Close() if userErr != nil { return userErr } return err } // InvokeModel provides an instrumented interface through which to call the AWS Bedrock InvokeModel function. // Where you would normally invoke the InvokeModel method on a bedrockruntime.Client value b from AWS as: // // b.InvokeModel(c, p, f...) // // You instead invoke the New Relic InvokeModel function as: // // nrbedrock.InvokeModel(app, b, c, p, f...) // // where app is the New Relic Application value returned from NewApplication when you started // your application. If you start a transaction and add it to the passed context value c in the above // invocation, the instrumentation will be recorded on that transaction, including a segment for the Bedrock // call itself. If you don't, a new transaction will be started for you, which will be terminated when the // InvokeModel function exits. // // If the transaction is unable to be created or used, the Bedrock call will be made anyway, without instrumentation. func InvokeModel(app *newrelic.Application, brc Modeler, ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) { return InvokeModelWithAttributes(app, brc, ctx, params, nil, optFns...) } // InvokeModelWithAttributes is identical to InvokeModel except for the addition of the attrs parameter, which is a // map of strings to values of any type. This map holds any custom attributes you wish to add to the reported metrics // relating to this model invocation. // // Each key in the attrs map must begin with "llm."; if any of them do not, "llm." is automatically prepended to // the attribute key before the metrics are sent out. // // We recommend including at least "llm.conversation_id" in your attributes. func InvokeModelWithAttributes(app *newrelic.Application, brc Modeler, ctx context.Context, params *bedrockruntime.InvokeModelInput, attrs map[string]any, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) { var txn *newrelic.Transaction // the transaction to record in, or nil if we aren't instrumenting this time var err error aiEnabled, recordContentEnabled := isEnabled(app, false) if aiEnabled { txn = newrelic.FromContext(ctx) if txn == nil { if txn = app.StartTransaction("InvokeModel"); txn != nil { defer txn.End() } } } var embedding bool id_key := "completion_id" if txn != nil { integrationsupport.AddAgentAttribute(txn, "llm", "", true) if params.ModelId != nil { if embedding = strings.Contains(*params.ModelId, "embed"); embedding { defer txn.StartSegment("Llm/embedding/Bedrock/InvokeModel").End() id_key = "embedding_id" } else { defer txn.StartSegment("Llm/completion/Bedrock/InvokeModel").End() } } else { // we don't have a model! txn = nil } } start := time.Now() output, err := brc.InvokeModel(ctx, params, optFns...) duration := time.Since(start).Milliseconds() if txn != nil { md := txn.GetTraceMetadata() uuid := uuid.New() meta := map[string]any{ "id": uuid.String(), "span_id": md.SpanID, "trace_id": md.TraceID, "request.model": *params.ModelId, "response.model": *params.ModelId, "vendor": "bedrock", "ingest_source": "Go", "duration": duration, } if err != nil { txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "BedrockError", Attributes: map[string]any{ id_key: uuid.String(), }, }) meta["error"] = true } var modelInput, modelOutput []byte if params != nil && params.Body != nil { modelInput = params.Body } if output != nil && output.Body != nil { modelOutput = output.Body } inputs, outputs, systemMessage := parseModelData(app, *params.ModelId, meta, modelInput, modelOutput, attrs, true) // To be more runtime efficient, we don't copy the maps or rebuild them for each kind of message. // Instead, we build one map with most of the attributes common to all messages and then adjust as needed // when reporting out each metric. if embedding { for _, theInput := range inputs { if theInput.tokenCount > 0 { meta["token_count"] = theInput.tokenCount } else { delete(meta, "token_count") } if recordContentEnabled && theInput.input != "" { meta["input"] = theInput.input } else { delete(meta, "input") } app.RecordCustomEvent("LlmEmbedding", meta) } } else { messageQty := len(inputs) + len(outputs) messageSeq := 0 if systemMessage != "" { messageQty++ } meta["response.number_of_messages"] = messageQty app.RecordCustomEvent("LlmChatCompletionSummary", meta) delete(meta, "duration") meta["completion_id"] = meta["id"] delete(meta, "id") delete(meta, "response.number_of_messages") if systemMessage != "" { meta["sequence"] = messageSeq messageSeq++ meta["role"] = "system" if recordContentEnabled { meta["content"] = systemMessage } app.RecordCustomEvent("LlmChatCompletionMessage", meta) } maxIterations := len(inputs) if maxIterations < len(outputs) { maxIterations = len(outputs) } for i := 0; i < maxIterations; i++ { if i < len(inputs) { meta["sequence"] = messageSeq messageSeq++ if inputs[i].tokenCount > 0 { meta["token_count"] = inputs[i].tokenCount } else { delete(meta, "token_count") } if recordContentEnabled { meta["content"] = inputs[i].input } else { delete(meta, "content") } delete(meta, "is_response") delete(meta, "response.choices.finish_reason") meta["role"] = "user" app.RecordCustomEvent("LlmChatCompletionMessage", meta) } if i < len(outputs) { meta["sequence"] = messageSeq messageSeq++ if outputs[i].tokenCount > 0 { meta["token_count"] = outputs[i].tokenCount } else { delete(meta, "token_count") } if recordContentEnabled { meta["content"] = outputs[i].output } else { delete(meta, "content") } meta["role"] = "assistant" meta["is_response"] = true if outputs[i].completionReason != "" { meta["response.choices.finish_reason"] = outputs[i].completionReason } else { delete(meta, "response.choices.finish_reason") } app.RecordCustomEvent("LlmChatCompletionMessage", meta) } } } } return output, err } func parseModelData(app *newrelic.Application, modelID string, meta map[string]any, modelInput, modelOutput []byte, attrs map[string]any, countTokens bool) ([]modelInputList, []modelResultList, string) { inputs := []modelInputList{} outputs := []modelResultList{} // Go fishing in the request and response JSON strings to find values we want to // record with our instrumentation. Since each model can define its own set of // expected input and output data formats, we either have to specifically define // model-specific templates or try to heuristically find our values in the places // we'd expect given the existing patterns shown in the model set we have today. // // This implementation takes the latter approach so as to be as flexible as possible // and have a good chance to find the data we're looking for even in new models // that follow the same general pattern as those models that came before them. // // Thanks to the fact that the input and output can be a JSON data structure // of literally anything, there's a lot of type assertion shenanigans going on // below, as we unmarshal the JSON into a map[string]any at the top level, and // then explore the "any" values on the way down, asserting them to be the actual // expected types as needed. var requestData, responseData map[string]any var systemMessage string if modelInput != nil && json.Unmarshal(modelInput, &requestData) == nil { // if the input contains a messages list, we have multiple messages to record if rs, ok := requestData["messages"]; ok { if rss, ok := rs.([]any); ok { for _, em := range rss { if eachMessage, ok := em.(map[string]any); ok { var role string if r, ok := eachMessage["role"]; ok { role, _ = r.(string) } if cs, ok := eachMessage["content"]; ok { if css, ok := cs.([]any); ok { for _, ec := range css { if eachContent, ok := ec.(map[string]any); ok { if ty, ok := eachContent["type"]; ok { if typ, ok := ty.(string); ok && typ == "text" { if txt, ok := eachContent["text"]; ok { if txts, ok := txt.(string); ok { inputs = append(inputs, modelInputList{input: txts, role: role}) } } } } } } } } } } } } if sys, ok := requestData["system"]; ok { systemMessage, _ = sys.(string) } // otherwise, look for what the single or multiple prompt input is called var inputString string if s, ok := requestData["inputText"]; ok { inputString, _ = s.(string) } else if s, ok := requestData["prompt"]; ok { inputString, _ = s.(string) } else if ss, ok := requestData["texts"]; ok { if slist, ok := ss.([]string); ok { for _, inpStr := range slist { inputs = append(inputs, modelInputList{input: inpStr, role: "user"}) } } } if inputString != "" { inputs = append(inputs, modelInputList{input: inputString, role: "user"}) } if cfg, ok := requestData["textGenerationConfig"]; ok { if cfgMap, ok := cfg.(map[string]any); ok { if t, ok := cfgMap["temperature"]; ok { meta["request.temperature"] = t } if m, ok := cfgMap["maxTokenCount"]; ok { meta["request.max_tokens"] = m } } } else if t, ok := requestData["temperature"]; ok { meta["request.temperature"] = t } if m, ok := requestData["max_tokens_to_sample"]; ok { meta["request.max_tokens"] = m } else if m, ok := requestData["max_tokens"]; ok { meta["request.max_tokens"] = m } else if m, ok := requestData["maxTokens"]; ok { meta["request.max_tokens"] = m } else if m, ok := requestData["max_gen_len"]; ok { meta["request.max_tokens"] = m } } var stopReason string var outputString string if modelOutput != nil { if json.Unmarshal(modelOutput, &responseData) == nil { if len(inputs) == 0 { if s, ok := responseData["prompt"]; ok { if inpStr, ok := s.(string); ok { inputs = append(inputs, modelInputList{input: inpStr, role: "user"}) } } } if id, ok := responseData["id"]; ok { meta["request_id"] = id } if s, ok := responseData["stop_reason"]; ok { stopReason, _ = s.(string) } if out, ok := responseData["completion"]; ok { outputString, _ = out.(string) } if rs, ok := responseData["results"]; ok { if crs, ok := rs.([]any); ok { for _, crv := range crs { if crvv, ok := crv.(map[string]any); ok { var stopR, outputS string if reason, ok := crvv["completionReason"]; ok { stopR, _ = reason.(string) } if out, ok := crvv["outputText"]; ok { outputS, _ = out.(string) outputs = append(outputs, modelResultList{output: outputS, completionReason: stopR}) } } } } } // modelResultList{output: completionReason:} if rs, ok := responseData["completions"]; ok { if crs, ok := rs.([]any); ok { for _, crsv := range crs { if crv, ok := crsv.(map[string]any); ok { var outputR string if cdata, ok := crv["finishReason"]; ok { if cdatamap, ok := cdata.(map[string]any); ok { if reason, ok := cdatamap["reason"]; ok { outputR, _ = reason.(string) } } } if cdata, ok := crv["data"]; ok { if cdatamap, ok := cdata.(map[string]any); ok { if out, ok := cdatamap["text"]; ok { if outS, ok := out.(string); ok { outputs = append(outputs, modelResultList{output: outS, completionReason: outputR}) } } } } } } } } if rs, ok := responseData["outputs"]; ok { if crs, ok := rs.([]any); ok { for _, crvv := range crs { if crv, ok := crvv.(map[string]any); ok { var stopR string if reason, ok := crv["stop_reason"]; ok { stopR, _ = reason.(string) } if out, ok := crv["text"]; ok { if outS, ok := out.(string); ok { outputs = append(outputs, modelResultList{output: outS, completionReason: stopR}) } } } } } } if rs, ok := responseData["generations"]; ok { if crs, ok := rs.([]any); ok { for _, crvv := range crs { if crv, ok := crvv.(map[string]any); ok { var stopR string if reason, ok := crv["finish_reason"]; ok { stopR, _ = reason.(string) } if out, ok := crv["text"]; ok { if outS, ok := out.(string); ok { outputs = append(outputs, modelResultList{output: outS, completionReason: stopR}) } } } } } } if outputString == "" { if out, ok := responseData["generation"]; ok { outputString, _ = out.(string) } } if outputString != "" { outputs = append(outputs, modelResultList{output: outputString, completionReason: stopReason}) } } } if attrs != nil { for k, v := range attrs { if strings.HasPrefix(k, "llm.") { meta[k] = v } else { meta["llm."+k] = v } } } if countTokens && app.HasLLMTokenCountCallback() { for i, _ := range inputs { if inputs[i].input != "" { inputs[i].tokenCount, _ = app.InvokeLLMTokenCountCallback(modelID, inputs[i].input) } } for i, _ := range outputs { if outputs[i].output != "" { outputs[i].tokenCount, _ = app.InvokeLLMTokenCountCallback(modelID, outputs[i].output) } } } return inputs, outputs, systemMessage } /*** We support: Anthropic Claude anthropic.claude-v2 anthropic.claude-v2:1 anthropic.claude-3-sonnet-... anthropic.claude-3-haiku-... anthropic.claude-instant-v1 Amazon Titan amazon.titan-text-express-v1 amazon.titan-text-lite-v1 E amazon.titan-embed-text-v1 Meta Llama 2 meta.llama2-13b-chat-v1 meta.llama2-70b-chat-v1 Cohere Command cohere.command-text-v14 cohere.command-light-text-v14 E cohere.embed-english-v3 E cohere.embed-multilingual-v3 texts:[string] embeddings:[1024 floats] input_type:s => id:s truncate:s response_type:s texts:[s] AI21 Labs Jurassic ai21.j2-mid-v1 ai21.j2-ultra-v1 only text-based models send LLM events as custom events ONLY when there is a transaction active attrs limited to 4095 normally but LLM events are an exception to this. NO limits. MAY limit other but MUST leave these unlimited: LlmChatCompletionMessage event, attr content LlmEmbedding event, attr input Events recorded: LlmEmbedding (creation of an embedding) id UUID we generate request_id from response headers usually span_id GUID assoc'd with activespan trace_id current trace ID input input to the embedding creation call request.model model name e.g. gpt-3.5-turbo response.model model name returned in response response.organization org ID returned in response or headers token_count value from LLMTokenCountCallback or omitted vendor "bedrock" ingest_source "Go" duration total time taken for chat completiong in mS error true if error occurred or omitted llm. **custom** response.headers. **response** LlmChatCompletionSummary (high-level data about creation of chat completion including request, response, and call info) id UUID we generate request_id from response headers usually span_id GUID assoc'd with active span trace_id current trace ID request.temperature how random/deterministic output shoudl be request.max_tokens max #tokens that can be generated request.model model name e.g. gpt-3.5-turbo response.model model name returned in response response.number_of_messages number of msgs comprising completiong response.choices.finish_reason reason model stopped (e.g. "stop") vendor "bedrock" ingest_source "Go" duration total time taken for chat completiong in mS error true if error occurred or omitted llm. **custom** response.headers. **response** LlmChatCompletionMessage (each message sent/rec'd from chat completion call. id UUID we generate OR - returned by LLM request_id from response headers usually span_id GUID assoc'd with active span trace_id current trace ID ??request.model model name e.g. gpt-3.5-turbo response.model model name returned in response vendor "bedrock" ingest_source "Go" content content of msg role role of msg creator sequence index (0..) w/each msg including prompt and responses completion_id ID of LlmChatCompletionSummary event that event is connected to is_response true if msg is result of completion, not input msg OR omitted token_count value from LLMTokenCountCallback or omitted llm. **custom** response.model = request.model if we don't get a response.model custom attributes to LLM events have llm. prefix and this should be retained llm.conversation_id **custom** user may add custom attributes to txn but we MUST strip out all that don't start with "llm." we recommend adding llm.conversation_id since that has UI implications **response** Capture response header values and add them as attributes to LLMEmbedding and LLMChatCompletionSummary events as "response.headers." if present, omit any that are not present. OpenAI: llmVersion, ratelimitLimitRequests, ratelimitResetTokens, ratelimitLimitTokens, ratelimitRemainingTokens, ratelimitRemainingRequests, ratelimitLimitTokensUsageBased, ratelimitResetTokensUsageBased, ratelimitRemainingTokensUsageBased Bedrock: ?? MUST add "llm: True" as agent attr to txn that contain instrumented LLM functions. MUST be sent to txn events attr dest (DST_TRANSACTION_EVENTS). OMIT if there are no LLM events in the txn. MUST create span for each LLM embedding and chat completion call. MUST only be created if there is a txn. MUST name them "Llm/completion|embedding/Bedrock/invoke_model|create|etc" Errors -> notice_error http.statusCode, error.code (exception), error.param (exception), completion_id, embedding_id STILL create LlmChatCompletionSummary and LlmEmbedding events in error context with all attrs that can be captured, plus set error=true. Supportability Metric X Supportability/Go/Bedrock/ X Supportability/Go/ML/Streaming/Disabled if !ai_monitoring.streaming.enabled Config ai_monitoring.enabled ai_monitoring.streaming.enabled ai_monitoring.record_content.enabled If true, suppress LlmChatCompletionMessage.content LlmEmbedding.imput LlmTool.input LlmTool.output LlmVectorSearch.request.query LlmVectorSearchResult.page_content Feedback tracked on trace ID API: getCurrentTraceID() or something to get the ID of the current active trace OR use pre-existing getLinkingMetadata to pull from map of returned data values **this means DT must be enabled to use feedback API: RecordLLMFeedbackEvent() -> custom event which includes end user feedback data API: LLMTokenCountCallback() to get the token count pass model name (string), content of message/prompt (string) receive integer count value -> token_count attr in LlmChatCompletionMessage or LlmEmbedding event UNLESS value <= 0, in which case ignore it. API: function to register the callback function, allowed to replace with a new one at any time. New models mistral.mistral-7b-instruct-v0:2, mistral.mixtral-8x7b-instruct-v0:1 support? -> body looks like { 'prompt': , 'max_tokens': 'temperature': } openai response headers include these but not always since they aren't always present ratelimitLimitTokensUsageBased ratelimitResetTokensUsageBased ratelimitRemainingTokensUsageBased ModelResultList Output CompletionReason TokenCount ModelInputList Role Input amazon titan out: results[] outputText, completionReason stream: chunk/bytes/index, outputText, completionReason Claude in: messages[] role, content[] type='text', text system: "system message" out: content[] type="text", text stop_reason Cohere: out: generations[] finish_reason, id, text, index? id prompt Mistral out: outputs[] text, stop_reason ***/ go-agent-3.42.0/v3/integrations/nrawssdk-v1/000077500000000000000000000000001510742411500205175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrawssdk-v1/LICENSE.txt000066400000000000000000000264501510742411500223510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrawssdk-v1/README.md000066400000000000000000000007261510742411500220030ustar00rootroot00000000000000# v3/integrations/nrawssdk-v1 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1) Package `nrawssdk` instruments https://github.com/aws/aws-sdk-go requests. ```go import "github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1). go-agent-3.42.0/v3/integrations/nrawssdk-v1/go.mod000066400000000000000000000007321510742411500216270ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1 // As of Dec 2019, aws-sdk-go's go.mod does not specify a Go version. 1.6 is // the earliest version of Go tested by aws-sdk-go's CI: // https://github.com/aws/aws-sdk-go/blob/master/.travis.yml go 1.24 require ( // v1.15.0 is the first aws-sdk-go version with module support. github.com/aws/aws-sdk-go v1.34.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrawssdk-v1/nrawssdk.go000066400000000000000000000062431510742411500227070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrawssdk instruments https://github.com/aws/aws-sdk-go requests. package nrawssdk import ( "github.com/aws/aws-sdk-go/aws/request" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/awssupport" ) func init() { internal.TrackUsage("integration", "library", "aws-sdk-go") } func startSegment(req *request.Request) { input := awssupport.StartSegmentInputs{ HTTPRequest: req.HTTPRequest, ServiceName: req.ClientInfo.ServiceName, Operation: req.Operation.Name, Region: req.ClientInfo.SigningRegion, Params: req.Params, } req.HTTPRequest = awssupport.StartSegment(input) } func endSegment(req *request.Request) { awssupport.EndSegment(req.HTTPRequest.Context(), req.HTTPResponse) } // InstrumentHandlers will add instrumentation to the given *request.Handlers. // // A Segment will be created for each out going request. The Transaction must // be added to the `http.Request`'s Context in order for the segment to be // recorded. For DynamoDB calls, these segments will be // `newrelic.DatastoreSegment` type and for all others they will be // `newrelic.ExternalSegment` type. // // Additional attributes will be added to Transaction Trace Segments and Span // Events: aws.region, aws.requestId, and aws.operation. // // To add instrumentation to the Session and see segments created for each // invocation that uses the Session, call InstrumentHandlers with the session's // Handlers and add the current Transaction to the `http.Request`'s Context: // // ses := session.New() // // Add instrumentation to handlers // nrawssdk.InstrumentHandlers(&ses.Handlers) // lambdaClient = lambda.New(ses, aws.NewConfig()) // // req, out := lambdaClient.InvokeRequest(&lambda.InvokeInput{ // ClientContext: aws.String("MyApp"), // FunctionName: aws.String("Function"), // InvocationType: aws.String("Event"), // LogType: aws.String("Tail"), // Payload: []byte("{}"), // } // // Add txn to http.Request's context // req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) // err := req.Send() // // To add instrumentation to a Request and see a segment created just for the // individual request, call InstrumentHandlers with the `request.Request`'s // Handlers and add the current Transaction to the `http.Request`'s Context: // // req, out := lambdaClient.InvokeRequest(&lambda.InvokeInput{ // ClientContext: aws.String("MyApp"), // FunctionName: aws.String("Function"), // InvocationType: aws.String("Event"), // LogType: aws.String("Tail"), // Payload: []byte("{}"), // } // // Add instrumentation to handlers // nrawssdk.InstrumentHandlers(&req.Handlers) // // Add txn to http.Request's context // req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) // err := req.Send() func InstrumentHandlers(handlers *request.Handlers) { handlers.Send.SetFrontNamed(request.NamedHandler{ Name: "StartNewRelicSegment", Fn: startSegment, }) handlers.Send.SetBackNamed(request.NamedHandler{ Name: "EndNewRelicSegment", Fn: endSegment, }) } go-agent-3.42.0/v3/integrations/nrawssdk-v1/nrawssdk_test.go000066400000000000000000000455471510742411500237600ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrawssdk import ( "bytes" "errors" "io/ioutil" "net/http" "strings" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/private/protocol/rest" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/lambda" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/awssupport" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func testApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } type fakeTransport struct{} func (t fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) { return &http.Response{ Status: "200 OK", StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), Header: http.Header{ "X-Amzn-Requestid": []string{requestID}, }, }, nil } type fakeCreds struct{} func (c *fakeCreds) Retrieve() (credentials.Value, error) { return credentials.Value{}, nil } func (c *fakeCreds) IsExpired() bool { return false } func newSession() *session.Session { r := "us-west-2" ses := session.New() ses.Config.Credentials = credentials.NewCredentials(&fakeCreds{}) ses.Config.HTTPClient.Transport = &fakeTransport{} ses.Config.Region = &r return ses } const ( requestID = "testing request id" txnName = "aws-txn" ) var ( genericSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/" + txnName, "transaction.name": "OtherTransaction/Go/" + txnName, "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "nr.entryPoint": true, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, } externalSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": "us-west-2", "aws.requestId": requestID, "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": 200, }, } externalSpanNoRequestID = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": "us-west-2", "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": 200, }, } externalSpanSendFailure = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": "us-west-2", "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": 0, }, } datastoreSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "Datastore/statement/DynamoDB/thebesttable/DescribeTable", "sampled": true, "category": "datastore", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "DynamoDB", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "DescribeTable", "aws.region": "us-west-2", "aws.requestId": requestID, "db.collection": "thebesttable", "db.statement": "'DescribeTable' on 'thebesttable' using 'DynamoDB'", "peer.address": "dynamodb.us-west-2.amazonaws.com:unknown", "peer.hostname": "dynamodb.us-west-2.amazonaws.com", }, } txnMetrics = []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, } externalMetrics = append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: nil}, }, txnMetrics...) datastoreMetrics = append([]internal.WantMetric{ {Name: "Datastore/DynamoDB/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/DynamoDB/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/instance/DynamoDB/dynamodb.us-west-2.amazonaws.com/unknown", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/DynamoDB/DescribeTable", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable/DescribeTable", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable/DescribeTable", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: nil}, }, txnMetrics...) ) func TestInstrumentRequestExternal(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) client := lambda.New(newSession()) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) InstrumentHandlers(&req.Handlers) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpan, genericSpan}) } func TestInstrumentRequestDatastore(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) client := dynamodb.New(newSession()) input := &dynamodb.DescribeTableInput{ TableName: aws.String("thebesttable"), } req, _ := client.DescribeTableRequest(input) InstrumentHandlers(&req.Handlers) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } txn.End() app.ExpectMetrics(t, datastoreMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ datastoreSpan, genericSpan}) } func TestInstrumentRequestExternalNoTxn(t *testing.T) { client := lambda.New(newSession()) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) InstrumentHandlers(&req.Handlers) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } } func TestInstrumentRequestDatastoreNoTxn(t *testing.T) { client := dynamodb.New(newSession()) input := &dynamodb.DescribeTableInput{ TableName: aws.String("thebesttable"), } req, _ := client.DescribeTableRequest(input) InstrumentHandlers(&req.Handlers) err := req.Send() if nil != err { t.Error(err) } } func TestInstrumentSessionExternal(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() InstrumentHandlers(&ses.Handlers) client := lambda.New(ses) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpan, genericSpan}) } func TestInstrumentSessionDatastore(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() InstrumentHandlers(&ses.Handlers) client := dynamodb.New(ses) input := &dynamodb.DescribeTableInput{ TableName: aws.String("thebesttable"), } req, _ := client.DescribeTableRequest(input) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } txn.End() app.ExpectMetrics(t, datastoreMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ datastoreSpan, genericSpan}) } func TestInstrumentSessionExternalNoTxn(t *testing.T) { ses := newSession() InstrumentHandlers(&ses.Handlers) client := lambda.New(ses) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, nil) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } } func TestInstrumentSessionDatastoreNoTxn(t *testing.T) { ses := newSession() InstrumentHandlers(&ses.Handlers) client := dynamodb.New(ses) input := &dynamodb.DescribeTableInput{ TableName: aws.String("thebesttable"), } req, _ := client.DescribeTableRequest(input) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, nil) err := req.Send() if nil != err { t.Error(err) } } func TestInstrumentSessionExternalTxnNotInCtx(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() InstrumentHandlers(&ses.Handlers) client := lambda.New(ses) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, txnMetrics) } func TestInstrumentSessionDatastoreTxnNotInCtx(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() InstrumentHandlers(&ses.Handlers) client := dynamodb.New(ses) input := &dynamodb.DescribeTableInput{ TableName: aws.String("thebesttable"), } req, _ := client.DescribeTableRequest(input) err := req.Send() if nil != err { t.Error(err) } txn.End() app.ExpectMetrics(t, txnMetrics) } func TestDoublyInstrumented(t *testing.T) { hs := &request.Handlers{} if found := hs.Send.Len(); 0 != found { t.Error("unexpected number of Send handlers found:", found) } InstrumentHandlers(hs) if found := hs.Send.Len(); 2 != found { t.Error("unexpected number of Send handlers found:", found) } InstrumentHandlers(hs) if found := hs.Send.Len(); 2 != found { t.Error("unexpected number of Send handlers found:", found) } } type firstFailingTransport struct { failing bool } func (t *firstFailingTransport) RoundTrip(r *http.Request) (*http.Response, error) { if t.failing { t.failing = false return nil, errors.New("Oops this failed") } return &http.Response{ Status: "200 OK", StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), Header: http.Header{ "X-Amzn-Requestid": []string{requestID}, }, }, nil } func TestRetrySend(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() ses.Config.HTTPClient.Transport = &firstFailingTransport{failing: true} client := lambda.New(ses) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) InstrumentHandlers(&req.Handlers) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpanSendFailure, externalSpan, genericSpan}) } func TestRequestSentTwice(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) client := lambda.New(newSession()) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) InstrumentHandlers(&req.Handlers) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) firstErr := req.Send() if nil != firstErr { t.Error(firstErr) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } secondErr := req.Send() if nil != secondErr { t.Error(secondErr) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: []float64{2}}, {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{2}}, {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: []float64{2}}, {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: []float64{2}}, {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpan, externalSpan, genericSpan}) } type noRequestIDTransport struct{} func (t *noRequestIDTransport) RoundTrip(r *http.Request) (*http.Response, error) { return &http.Response{ Status: "200 OK", StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), }, nil } func TestNoRequestIDFound(t *testing.T) { app := testApp() txn := app.StartTransaction(txnName) ses := newSession() ses.Config.HTTPClient.Transport = &noRequestIDTransport{} client := lambda.New(ses) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: aws.String("Event"), LogType: aws.String("Tail"), Payload: []byte("{}"), } req, out := client.InvokeRequest(input) InstrumentHandlers(&req.Handlers) req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) err := req.Send() if nil != err { t.Error(err) } if 200 != *out.StatusCode { t.Error("wrong status code on response", out.StatusCode) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpanNoRequestID, genericSpan}) } func TestGetRequestID(t *testing.T) { primary := "X-Amzn-Requestid" secondary := "X-Amz-Request-Id" testcases := []struct { hdr http.Header expected string }{ {hdr: http.Header{ "hello": []string{"world"}, }, expected: ""}, {hdr: http.Header{ strings.ToUpper(primary): []string{"hello"}, }, expected: ""}, {hdr: http.Header{ primary: []string{"hello"}, }, expected: "hello"}, {hdr: http.Header{ secondary: []string{"hello"}, }, expected: "hello"}, {hdr: http.Header{ primary: []string{"hello"}, secondary: []string{"world"}, }, expected: "hello"}, {hdr: http.Header{}, expected: ""}, } // Make sure our assumptions still hold against aws-sdk-go for _, test := range testcases { req := &request.Request{ HTTPResponse: &http.Response{ Header: test.hdr, }, } rest.UnmarshalMeta(req) if out := awssupport.GetRequestID(test.hdr); req.RequestID != out { t.Error("requestId assumptions incorrect", out, req.RequestID, test.hdr, test.expected) } } } go-agent-3.42.0/v3/integrations/nrawssdk-v2/000077500000000000000000000000001510742411500205205ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrawssdk-v2/LICENSE.txt000066400000000000000000000264501510742411500223520ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrawssdk-v2/README.md000066400000000000000000000007311510742411500220000ustar00rootroot00000000000000# v3/integrations/nrawssdk-v2 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2) Package `nrawssdk` instruments https://github.com/aws/aws-sdk-go-v2 requests. ```go import "github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2). go-agent-3.42.0/v3/integrations/nrawssdk-v2/example/000077500000000000000000000000001510742411500221535ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrawssdk-v2/example/main.go000066400000000000000000000035411510742411500234310ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // Create a New Relic application. This will look for your license key in an // environment variable called NEW_RELIC_LICENSE_KEY. This example turns on // Distributed Tracing, but that's not required. app, err := newrelic.NewApplication( newrelic.ConfigFromEnvironment(), newrelic.ConfigAppName("Example App"), newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if nil != err { fmt.Println(err) os.Exit(1) } // For demo purposes only. Don't use the app.WaitForConnection call in // production unless this is a very short-lived process and the caller // doesn't block or exit if there's an error. app.WaitForConnection(5 * time.Second) // Start recording a New Relic transaction txn := app.StartTransaction("My sample transaction") ctx := context.Background() awsConfig, err := config.LoadDefaultConfig(ctx, func(awsConfig *config.LoadOptions) error { // Instrument all new AWS clients with New Relic nrawssdk.AppendMiddlewares(&awsConfig.APIOptions, nil) return nil }) if err != nil { log.Fatal(err) } s3Client := s3.NewFromConfig(awsConfig) output, err := s3Client.ListBuckets(ctx, nil) if err != nil { log.Fatal(err) } for _, object := range output.Buckets { log.Printf("Bucket name is %s\n", aws.ToString(object.Name)) } // End the New Relic transaction txn.End() // Force all the harvests and shutdown. Like the app.WaitForConnection call // above, this is for the purposes of this demo only and can be safely // removed for longer-running processes. app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrawssdk-v2/go.mod000066400000000000000000000011561510742411500216310ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2 // As of May 2021, the aws-sdk-go-v2 go.mod file uses 1.15: // https://github.com/aws/aws-sdk-go-v2/blob/master/go.mod go 1.24 require ( github.com/aws/aws-sdk-go-v2 v1.30.4 github.com/aws/aws-sdk-go-v2/config v1.27.31 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.6 github.com/aws/aws-sdk-go-v2/service/lambda v1.58.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.61.0 github.com/aws/aws-sdk-go-v2/service/sqs v1.34.6 github.com/aws/smithy-go v1.20.4 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrawssdk-v2/nrawssdk.go000066400000000000000000000233011510742411500227020ustar00rootroot00000000000000// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package nrawssdk instruments requests made by the // https://github.com/aws/aws-sdk-go-v2 library. // // For most operations, external segments and spans are automatically created // for display in the New Relic UI on the External services section. For // DynamoDB operations, datastore segements and spans are created and will be // displayed on the Databases page. All operations will also be displayed on // transaction traces and distributed traces. // // To use this integration, simply apply the AppendMiddlewares fuction to the apiOptions in // your AWS Config object before performing any AWS operations. See // example/main.go for a working sample. package nrawssdk import ( "context" "net/url" "strconv" "strings" "github.com/aws/aws-sdk-go-v2/aws" awsmiddle "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/smithy-go/middleware" smithymiddle "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) type nrMiddleware struct { txn *newrelic.Transaction } type contextKey string const ( dynamodbInputKey contextKey = "DynamoDBInput" queueURLKey contextKey = "QueueURL" ) type endable interface{ End() } // See https://aws.github.io/aws-sdk-go-v2/docs/middleware/ for a description of // AWS SDK V2 middleware. func (m nrMiddleware) deserializeMiddleware(stack *smithymiddle.Stack) error { return stack.Deserialize.Add(smithymiddle.DeserializeMiddlewareFunc("NRDeserializeMiddleware", func( ctx context.Context, in smithymiddle.DeserializeInput, next smithymiddle.DeserializeHandler) ( out smithymiddle.DeserializeOutput, metadata smithymiddle.Metadata, err error) { txn := m.txn if txn == nil { txn = newrelic.FromContext(ctx) } smithyRequest := in.Request.(*smithyhttp.Request) // The actual http.Request is inside the smithyhttp.Request httpRequest := smithyRequest.Request serviceName := awsmiddle.GetServiceID(ctx) operation := awsmiddle.GetOperationName(ctx) region := awsmiddle.GetRegion(ctx) var segment endable if serviceName == "dynamodb" || serviceName == "DynamoDB" { input, _ := ctx.Value(dynamodbInputKey).(dynamodbInput) collection := input.tableName if input.indexName != "" { collection += "." + input.indexName } segment = &newrelic.DatastoreSegment{ Product: newrelic.DatastoreDynamoDB, Collection: collection, Operation: operation, ParameterizedQuery: "", QueryParameters: nil, Host: httpRequest.URL.Host, PortPathOrID: httpRequest.URL.Port(), DatabaseName: "", StartTime: txn.StartSegmentNow(), } } else { segment = newrelic.StartExternalSegment(txn, httpRequest) } // Hand off execution to other middlewares and then perform the request out, metadata, err = next.HandleDeserialize(ctx, in) // After the request response, ok := out.RawResponse.(*smithyhttp.Response) if ok { if serviceName == "sqs" || serviceName == "SQS" { if queueURL, ok := ctx.Value(queueURLKey).(string); ok { parsedURL, err := url.Parse(queueURL) if err == nil { // Example URL: https://sqs.{region}.amazonaws.com/{account.id}/{queue.name} pathParts := strings.Split(parsedURL.Path, "/") if len(pathParts) >= 3 { accountID := pathParts[1] queueName := pathParts[2] integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeCloudAccountID, accountID) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeCloudRegion, region) integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageSystem, "aws_sqs") integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageDestinationName, queueName) } } } } // Set additional span attributes integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeResponseCode, strconv.Itoa(response.StatusCode)) integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, operation) integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSRegion, region) requestID, ok := awsmiddle.GetRequestIDMetadata(metadata) if ok { integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeAWSRequestID, requestID) } } segment.End() return out, metadata, err }), smithymiddle.Before) } func (m nrMiddleware) serializeMiddleware(stack *middleware.Stack) error { return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("NRSerializeMiddleware", func( ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) ( out middleware.InitializeOutput, metadata middleware.Metadata, err error) { serviceName := awsmiddle.GetServiceID(ctx) switch serviceName { case "dynamodb", "DynamoDB": ctx = context.WithValue(ctx, dynamodbInputKey, dynamoDBInputFromMiddlewareInput(in)) case "sqs", "SQS": ctx = context.WithValue(ctx, queueURLKey, sqsQueueURFromMiddlewareInput(in)) } return next.HandleInitialize(ctx, in) }), middleware.After) } // AppendMiddlewares inserts New Relic middleware in the given `apiOptions` for // the AWS SDK V2 for Go. It must be called only once per AWS configuration. // // If `txn` is provided as nil, the New Relic transaction will be retrieved // using `newrelic.FromContext`. // // Additional attributes will be added to transaction trace segments and span // events: aws.region, aws.requestId, and aws.operation. In addition, // http.statusCode will be added to span events. // // To see segments and spans for all AWS invocations, call AppendMiddlewares // with the AWS Config `apiOptions` and provide nil for `txn`. For example: // // awsConfig, err := config.LoadDefaultConfig(ctx, func(o *config.LoadOptions) error { // // Instrument all new AWS clients with New Relic // nrawssdk.AppendMiddlewares(&o.APIOptions, nil) // return nil // }) // if err != nil { // log.Fatal(err) // } // // If do not want the transaction to be retrieved from the context, you can // explicitly set `txn`. For example: // // txn := loadNewRelicTransaction() // awsConfig, err := config.LoadDefaultConfig(ctx, func(o *config.LoadOptions) error { // // Instrument all new AWS clients with New Relic // nrawssdk.AppendMiddlewares(&o.APIOptions, txn) // return nil // }) // if err != nil { // log.Fatal(err) // } // // The middleware can also be added later, per AWS service call using // the `optFns` parameter. For example: // // awsConfig, err := config.LoadDefaultConfig(ctx) // if err != nil { // log.Fatal(err) // } // // ... // // s3Client := s3.NewFromConfig(awsConfig) // // ... // // txn := loadNewRelicTransaction() // output, err := s3Client.ListBuckets(ctx, nil, func(o *s3.Options) error { // nrawssdk.AppendMiddlewares(&o.APIOptions, txn) // return nil // }) // if err != nil { // log.Fatal(err) // } func AppendMiddlewares(apiOptions *[]func(*smithymiddle.Stack) error, txn *newrelic.Transaction) { m := nrMiddleware{txn: txn} *apiOptions = append(*apiOptions, m.deserializeMiddleware) *apiOptions = append(*apiOptions, m.serializeMiddleware) } func sqsQueueURFromMiddlewareInput(in middleware.InitializeInput) string { switch params := in.Parameters.(type) { case *sqs.SendMessageInput: return aws.ToString(params.QueueUrl) case *sqs.DeleteQueueInput: return aws.ToString(params.QueueUrl) case *sqs.ReceiveMessageInput: return aws.ToString(params.QueueUrl) case *sqs.DeleteMessageInput: return aws.ToString(params.QueueUrl) case *sqs.ChangeMessageVisibilityInput: return aws.ToString(params.QueueUrl) case *sqs.ChangeMessageVisibilityBatchInput: return aws.ToString(params.QueueUrl) case *sqs.DeleteMessageBatchInput: return aws.ToString(params.QueueUrl) case *sqs.SendMessageBatchInput: return aws.ToString(params.QueueUrl) case *sqs.PurgeQueueInput: return aws.ToString(params.QueueUrl) case *sqs.GetQueueAttributesInput: return aws.ToString(params.QueueUrl) case *sqs.SetQueueAttributesInput: return aws.ToString(params.QueueUrl) case *sqs.TagQueueInput: return aws.ToString(params.QueueUrl) case *sqs.UntagQueueInput: return aws.ToString(params.QueueUrl) default: return "" } } type dynamodbInput struct { tableName string indexName string } func dynamoDBInputFromMiddlewareInput(in middleware.InitializeInput) dynamodbInput { switch params := in.Parameters.(type) { case *dynamodb.DeleteItemInput: return dynamodbInput{tableName: aws.ToString(params.TableName)} case *dynamodb.GetItemInput: return dynamodbInput{tableName: aws.ToString(params.TableName)} case *dynamodb.PutItemInput: return dynamodbInput{tableName: aws.ToString(params.TableName)} case *dynamodb.QueryInput: return dynamodbInput{tableName: aws.ToString(params.TableName), indexName: aws.ToString(params.IndexName)} case *dynamodb.ScanInput: return dynamodbInput{tableName: aws.ToString(params.TableName), indexName: aws.ToString(params.IndexName)} case *dynamodb.UpdateItemInput: return dynamodbInput{tableName: aws.ToString(params.TableName)} default: return dynamodbInput{} } } go-agent-3.42.0/v3/integrations/nrawssdk-v2/nrawssdk_test.go000066400000000000000000001042721510742411500237500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrawssdk import ( "bytes" "context" "errors" "io" "net/http" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/lambda" lambdatypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" "github.com/aws/aws-sdk-go-v2/service/sqs" sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func testApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } type fakeTransport struct{} func (t fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) { return &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(""))), Header: http.Header{ "X-Amzn-Requestid": []string{requestID}, }, }, nil } type fakeCredsWithoutContext struct{} func (c fakeCredsWithoutContext) Retrieve() (aws.Credentials, error) { return aws.Credentials{}, nil } type fakeCredsWithContext struct{} func (c fakeCredsWithContext) Retrieve(ctx context.Context) (aws.Credentials, error) { return aws.Credentials{}, nil } var fakeCreds = func() interface{} { var c interface{} = fakeCredsWithoutContext{} if _, ok := c.(aws.CredentialsProvider); ok { return c } return fakeCredsWithContext{} }() func newConfig(ctx context.Context, txn *newrelic.Transaction) aws.Config { cfg, _ := config.LoadDefaultConfig(ctx, func(o *config.LoadOptions) error { AppendMiddlewares(&o.APIOptions, txn) return nil }) cfg.Credentials = fakeCreds.(aws.CredentialsProvider) cfg.Region = awsRegion cfg.HTTPClient = &http.Client{ Transport: &fakeTransport{}, } return cfg } const ( requestID = "testing request id" txnName = "aws-txn" awsRegion = "us-west-2" ) var ( genericSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/" + txnName, "transaction.name": "OtherTransaction/Go/" + txnName, "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "nr.entryPoint": true, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, } externalSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": awsRegion, "aws.requestId": requestID, "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": "200", }, } externalSpanNoRequestID = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": awsRegion, "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": "200", }, } SQSSpan = internal.WantEvent{ Intrinsics: map[string]interface{}{ "name": "External/sqs.us-west-2.amazonaws.com/http/POST", "category": "http", "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", "sampled": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "message.destination.name": "MyQueue", "cloud.account.id": "123456789012", "cloud.region": "us-west-2", "http.url": "https://sqs.us-west-2.amazonaws.com/", "http.method": "POST", "messaging.system": "aws_sqs", "aws.requestId": "testing request id", "http.statusCode": "200", "aws.region": "us-west-2", }, } txnMetrics = []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, } externalMetrics = append(txnMetrics, []internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: nil}, }...) ) type testTableEntry struct { Name string BuildContext func(txn *newrelic.Transaction) context.Context BuildConfig func(ctx context.Context, txn *newrelic.Transaction) aws.Config } func runTestTable(t *testing.T, table []*testTableEntry, executeEntry func(t *testing.T, entry *testTableEntry)) { for _, entry := range table { entry := entry // Pin range variable t.Run(entry.Name, func(t *testing.T) { executeEntry(t, entry) }) } } func TestInstrumentRequestExternal(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) client := lambda.NewFromConfig(entry.BuildConfig(ctx, txn)) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: lambdatypes.InvocationTypeRequestResponse, LogType: lambdatypes.LogTypeTail, Payload: []byte("{}"), } _, err := client.Invoke(ctx, input) if err != nil { t.Error(err) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpan, genericSpan}) }, ) } type sqsTestTableEntry struct { Name string BuildContext func(txn *newrelic.Transaction) context.Context BuildConfig func(ctx context.Context, txn *newrelic.Transaction) aws.Config Input interface{} } func runSQSTestTable(t *testing.T, entries []*sqsTestTableEntry, testFunc func(t *testing.T, entry *sqsTestTableEntry)) { for _, entry := range entries { t.Run(entry.Name, func(t *testing.T) { testFunc(t, entry) }) } } func TestSQSMiddleware(t *testing.T) { runSQSTestTable(t, []*sqsTestTableEntry{ { Name: "DeleteQueueInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.DeleteQueueInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue")}, }, { Name: "ReceiveMessageInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.ReceiveMessageInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue")}, }, { Name: "SendMessageInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.SendMessageInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), MessageBody: aws.String("Hello, world!")}, }, { Name: "PurgeQueueInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.PurgeQueueInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue")}, }, { Name: "DeleteMessageInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.DeleteMessageInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), ReceiptHandle: aws.String("receipt-handle")}, }, { Name: "ChangeMessageVisibilityInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.ChangeMessageVisibilityInput{QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), ReceiptHandle: aws.String("receipt-handle"), VisibilityTimeout: 10}, }, { Name: "ChangeMessageVisibilityBatchInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.ChangeMessageVisibilityBatchInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), Entries: []sqstypes.ChangeMessageVisibilityBatchRequestEntry{ { Id: aws.String("id1"), ReceiptHandle: aws.String("receipt-handle"), VisibilityTimeout: 10, }, }, }, }, { Name: "DeleteMessageBatchInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.DeleteMessageBatchInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), Entries: []sqstypes.DeleteMessageBatchRequestEntry{ { Id: aws.String("id1"), ReceiptHandle: aws.String("receipt-handle"), }, }, }, }, { Name: "SendMessageBatchInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.SendMessageBatchInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), Entries: []sqstypes.SendMessageBatchRequestEntry{ { Id: aws.String("id1"), MessageBody: aws.String("Hello, world!"), }, }, }, }, { Name: "GetQueueAttributesInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.GetQueueAttributesInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), AttributeNames: []sqstypes.QueueAttributeName{ "ApproximateNumberOfMessages", }, }, }, { Name: "SetQueueAttributesInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.SetQueueAttributesInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), Attributes: map[string]string{ "VisibilityTimeout": "10", }, }, }, { Name: "TagQueueInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.TagQueueInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), Tags: map[string]string{ "tag1": "value1", }, }, }, { Name: "UntagQueueInput", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, txn) }, Input: &sqs.UntagQueueInput{ QueueUrl: aws.String("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), TagKeys: []string{"tag1"}, }, }, }, func(t *testing.T, entry *sqsTestTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) awsOp := "" client := sqs.NewFromConfig(entry.BuildConfig(ctx, txn)) switch input := entry.Input.(type) { case *sqs.SendMessageInput: client.SendMessage(ctx, input) awsOp = "SendMessage" case *sqs.DeleteQueueInput: client.DeleteQueue(ctx, input) awsOp = "DeleteQueue" case *sqs.ReceiveMessageInput: client.ReceiveMessage(ctx, input) awsOp = "ReceiveMessage" case *sqs.DeleteMessageInput: client.DeleteMessage(ctx, input) awsOp = "DeleteMessage" case *sqs.ChangeMessageVisibilityInput: client.ChangeMessageVisibility(ctx, input) awsOp = "ChangeMessageVisibility" case *sqs.ChangeMessageVisibilityBatchInput: client.ChangeMessageVisibilityBatch(ctx, input) awsOp = "ChangeMessageVisibilityBatch" case *sqs.DeleteMessageBatchInput: client.DeleteMessageBatch(ctx, input) awsOp = "DeleteMessageBatch" case *sqs.PurgeQueueInput: client.PurgeQueue(ctx, input) awsOp = "PurgeQueue" case *sqs.GetQueueAttributesInput: client.GetQueueAttributes(ctx, input) awsOp = "GetQueueAttributes" case *sqs.SetQueueAttributesInput: client.SetQueueAttributes(ctx, input) awsOp = "SetQueueAttributes" case *sqs.TagQueueInput: client.TagQueue(ctx, input) awsOp = "TagQueue" case *sqs.UntagQueueInput: client.UntagQueue(ctx, input) awsOp = "UntagQueue" case *sqs.SendMessageBatchInput: client.SendMessageBatch(ctx, input) awsOp = "SendMessageBatch" default: t.Errorf("unexpected input type: %T", input) } txn.End() SQSSpanModified := SQSSpan SQSSpanModified.AgentAttributes["aws.operation"] = awsOp app.ExpectSpanEvents(t, []internal.WantEvent{ SQSSpan, genericSpan}) }, ) } func TestInstrumentRequestDynamoDB(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) client := dynamodb.NewFromConfig(entry.BuildConfig(ctx, txn)) input := &dynamodb.GetItemInput{ Key: map[string]dynamodbtypes.AttributeValue{ "PartitionKey": &dynamodbtypes.AttributeValueMemberS{Value: "foo"}, }, TableName: aws.String("thebesttable"), } _, err := client.GetItem(ctx, input) if err != nil { t.Error(err) } txn.End() var datastoreMetrics []internal.WantMetric datastoreMetrics = append(datastoreMetrics, txnMetrics...) datastoreMetrics = append(datastoreMetrics, []internal.WantMetric{ {Name: "Datastore/DynamoDB/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/DynamoDB/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/instance/DynamoDB/dynamodb.us-west-2.amazonaws.com/unknown", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/DynamoDB/GetItem", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable/GetItem", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable/GetItem", Scope: "OtherTransaction/Go/aws-txn", Forced: false, Data: nil}, }...) app.ExpectMetrics(t, datastoreMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/statement/DynamoDB/thebesttable/GetItem", "sampled": true, "category": "datastore", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "DynamoDB", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "GetItem", "aws.region": awsRegion, "aws.requestId": requestID, "db.collection": "thebesttable", "db.statement": "'GetItem' on 'thebesttable' using 'DynamoDB'", "peer.address": "dynamodb.us-west-2.amazonaws.com:unknown", "peer.hostname": "dynamodb.us-west-2.amazonaws.com", "http.statusCode": "200", }, }, genericSpan, }) }, ) } func TestInstrumentRequestDynamoDBWithIndex(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) client := dynamodb.NewFromConfig(entry.BuildConfig(ctx, txn)) input := &dynamodb.ScanInput{ TableName: aws.String("thebesttable"), IndexName: aws.String("someindex"), } _, err := client.Scan(ctx, input) if err != nil { t.Error(err) } txn.End() var datastoreMetrics []internal.WantMetric datastoreMetrics = append(datastoreMetrics, txnMetrics...) datastoreMetrics = append(datastoreMetrics, []internal.WantMetric{ {Name: "Datastore/DynamoDB/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/DynamoDB/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/instance/DynamoDB/dynamodb.us-west-2.amazonaws.com/unknown", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/DynamoDB/Scan", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable.someindex/Scan", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/DynamoDB/thebesttable.someindex/Scan", Scope: "OtherTransaction/Go/aws-txn", Forced: false, Data: nil}, }...) app.ExpectMetrics(t, datastoreMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/statement/DynamoDB/thebesttable.someindex/Scan", "sampled": true, "category": "datastore", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "DynamoDB", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Scan", "aws.region": awsRegion, "aws.requestId": requestID, "db.collection": "thebesttable.someindex", "db.statement": "'Scan' on 'thebesttable.someindex' using 'DynamoDB'", "peer.address": "dynamodb.us-west-2.amazonaws.com:unknown", "peer.hostname": "dynamodb.us-west-2.amazonaws.com", "http.statusCode": "200", }, }, genericSpan, }) }, ) } func TestInstrumentRequestDynamoDBOther(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) client := dynamodb.NewFromConfig(entry.BuildConfig(ctx, txn)) input := &dynamodb.BatchGetItemInput{ RequestItems: map[string]dynamodbtypes.KeysAndAttributes{ "FirstTable": { Keys: []map[string]dynamodbtypes.AttributeValue{ {"PartitionKey": &dynamodbtypes.AttributeValueMemberS{Value: "foo"}}, }, }, "SecondTable": { Keys: []map[string]dynamodbtypes.AttributeValue{ {"PartitionKey": &dynamodbtypes.AttributeValueMemberS{Value: "bar"}}, }, }, }, } _, err := client.BatchGetItem(ctx, input) if err != nil { t.Error(err) } txn.End() var datastoreMetrics []internal.WantMetric datastoreMetrics = append(datastoreMetrics, txnMetrics...) datastoreMetrics = append(datastoreMetrics, []internal.WantMetric{ {Name: "Datastore/DynamoDB/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/DynamoDB/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/instance/DynamoDB/dynamodb.us-west-2.amazonaws.com/unknown", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/DynamoDB/BatchGetItem", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/DynamoDB/BatchGetItem", Scope: "OtherTransaction/Go/aws-txn", Forced: false, Data: nil}, }...) app.ExpectMetrics(t, datastoreMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/operation/DynamoDB/BatchGetItem", "sampled": true, "category": "datastore", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "DynamoDB", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "BatchGetItem", "aws.region": awsRegion, "aws.requestId": requestID, "db.statement": "'BatchGetItem' on 'unknown' using 'DynamoDB'", "peer.address": "dynamodb.us-west-2.amazonaws.com:unknown", "peer.hostname": "dynamodb.us-west-2.amazonaws.com", "http.statusCode": "200", }, }, genericSpan, }) }, ) } type firstFailingTransport struct { failing bool } func (t *firstFailingTransport) RoundTrip(r *http.Request) (*http.Response, error) { if t.failing { t.failing = false return nil, errors.New("Oops this failed") } return &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(""))), Header: http.Header{ "X-Amzn-Requestid": []string{requestID}, }, }, nil } func TestRetrySend(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) cfg := entry.BuildConfig(ctx, txn) cfg.HTTPClient = &http.Client{ Transport: &firstFailingTransport{failing: true}, } customRetry := retry.NewStandard(func(o *retry.StandardOptions) { o.MaxAttempts = 2 }) client := lambda.NewFromConfig(cfg, func(o *lambda.Options) { o.Retryer = customRetry }) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: lambdatypes.InvocationTypeRequestResponse, LogType: lambdatypes.LogTypeTail, Payload: []byte("{}"), } _, err := client.Invoke(ctx, input) if err != nil { t.Error(err) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": awsRegion, "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": "0", }, }, { Intrinsics: map[string]interface{}{ "name": "External/lambda.us-west-2.amazonaws.com/http/POST", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentId": internal.MatchAnything, "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "Invoke", "aws.region": awsRegion, "aws.requestId": requestID, "http.method": "POST", "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", "http.statusCode": "200", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/" + txnName, "transaction.name": "OtherTransaction/Go/" + txnName, "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, "nr.entryPoint": true, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }}) }, ) } func TestRequestSentTwice(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) client := lambda.NewFromConfig(entry.BuildConfig(ctx, txn)) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: lambdatypes.InvocationTypeRequestResponse, LogType: lambdatypes.LogTypeTail, Payload: []byte("{}"), } _, firstErr := client.Invoke(ctx, input) if firstErr != nil { t.Error(firstErr) } _, secondErr := client.Invoke(ctx, input) if secondErr != nil { t.Error(secondErr) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: []float64{2}}, {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{2}}, {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: []float64{2}}, {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: []float64{2}}, {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpan, externalSpan, genericSpan}) }, ) } type noRequestIDTransport struct{} func (t *noRequestIDTransport) RoundTrip(r *http.Request) (*http.Response, error) { return &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(""))), }, nil } func TestNoRequestIDFound(t *testing.T) { runTestTable(t, []*testTableEntry{ { Name: "with manually set transaction", BuildContext: func(txn *newrelic.Transaction) context.Context { return context.Background() }, BuildConfig: newConfig, }, { Name: "with transaction set in context", BuildContext: func(txn *newrelic.Transaction) context.Context { return newrelic.NewContext(context.Background(), txn) }, BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context }, }, }, func(t *testing.T, entry *testTableEntry) { app := testApp() txn := app.StartTransaction(txnName) ctx := entry.BuildContext(txn) cfg := entry.BuildConfig(ctx, txn) cfg.HTTPClient = &http.Client{ Transport: &noRequestIDTransport{}, } client := lambda.NewFromConfig(cfg) input := &lambda.InvokeInput{ ClientContext: aws.String("MyApp"), FunctionName: aws.String("non-existent-function"), InvocationType: lambdatypes.InvocationTypeRequestResponse, LogType: lambdatypes.LogTypeTail, Payload: []byte("{}"), } _, err := client.Invoke(ctx, input) if err != nil { t.Error(err) } txn.End() app.ExpectMetrics(t, externalMetrics) app.ExpectSpanEvents(t, []internal.WantEvent{ externalSpanNoRequestID, genericSpan}) }, ) } go-agent-3.42.0/v3/integrations/nrb3/000077500000000000000000000000001510742411500172035ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrb3/LICENSE.txt000066400000000000000000000264501510742411500210350ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrb3/README.md000066400000000000000000000006501510742411500204630ustar00rootroot00000000000000# v3/integrations/nrb3 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrb3?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrb3) Package `nrb3` supports adding B3 headers to outgoing requests. ```go import "github.com/newrelic/go-agent/v3/integrations/nrb3" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrb3). go-agent-3.42.0/v3/integrations/nrb3/example_test.go000066400000000000000000000017721510742411500222330ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrb3_test import ( "fmt" "log" "net/http" "github.com/newrelic/go-agent/v3/integrations/nrb3" "github.com/newrelic/go-agent/v3/newrelic" ) func currentTxn() *newrelic.Transaction { return nil } func ExampleNewRoundTripper() { // When defining the client, set the Transport to the NewRoundTripper. This // will create ExternalSegments and add B3 headers for each request. client := &http.Client{ Transport: nrb3.NewRoundTripper(nil), } // Distributed Tracing must be enabled for this application. txn := currentTxn() req, err := http.NewRequest("GET", "http://example.com", nil) if nil != err { log.Fatalln(err) } // Be sure to add the transaction to the request context. This step is // required. req = newrelic.RequestWithTransactionContext(req, txn) resp, err := client.Do(req) if nil != err { log.Fatalln(err) } defer resp.Body.Close() fmt.Println(resp.StatusCode) } go-agent-3.42.0/v3/integrations/nrb3/go.mod000066400000000000000000000002461510742411500203130ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrb3 go 1.24 require github.com/newrelic/go-agent/v3 v3.42.0 replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrb3/nrb3.go000066400000000000000000000045471510742411500204100ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrb3 import ( "net/http" "time" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "b3") } // NewRoundTripper creates an `http.RoundTripper` to instrument external // requests. The RoundTripper returned creates an external segment and adds B3 // tracing headers to each request if and only if a `newrelic.Transaction` // (https://godoc.org/github.com/newrelic/go-agent#Transaction) is found in the // `http.Request`'s context. It then delegates to the original RoundTripper // provided (or http.DefaultTransport if none is provided). func NewRoundTripper(original http.RoundTripper) http.RoundTripper { if nil == original { original = http.DefaultTransport } return &b3Transport{ idGen: internal.NewTraceIDGenerator(int64(time.Now().UnixNano())), original: original, } } // cloneRequest mimics implementation of // https://godoc.org/github.com/google/go-github/github#BasicAuthTransport.RoundTrip func cloneRequest(r *http.Request) *http.Request { // shallow copy of the struct r2 := new(http.Request) *r2 = *r // deep copy of the Header r2.Header = make(http.Header, len(r.Header)) for k, s := range r.Header { r2.Header[k] = append([]string(nil), s...) } return r2 } type b3Transport struct { idGen *internal.TraceIDGenerator original http.RoundTripper } func txnSampled(txn *newrelic.Transaction) string { if txn.IsSampled() { return "1" } return "0" } func addHeader(request *http.Request, key, val string) { if val != "" { request.Header.Add(key, val) } } func (t *b3Transport) RoundTrip(request *http.Request) (*http.Response, error) { if txn := newrelic.FromContext(request.Context()); nil != txn { // The specification of http.RoundTripper requires that the request is never modified. request = cloneRequest(request) segment := &newrelic.ExternalSegment{ StartTime: txn.StartSegmentNow(), Request: request, } defer segment.End() md := txn.GetTraceMetadata() addHeader(request, "X-B3-TraceId", md.TraceID) addHeader(request, "X-B3-SpanId", t.idGen.GenerateSpanID()) addHeader(request, "X-B3-ParentSpanId", md.SpanID) addHeader(request, "X-B3-Sampled", txnSampled(txn)) } return t.original.RoundTrip(request) } go-agent-3.42.0/v3/integrations/nrb3/nrb3_doc.go000066400000000000000000000032021510742411500212200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrb3 supports adding B3 headers to outgoing requests. // // When using the New Relic Go Agent, use this package if you want to add B3 // headers ("X-B3-TraceId", etc., see // https://github.com/openzipkin/b3-propagation) to outgoing requests. // // Distributed tracing must be enabled // (https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing) // for B3 headers to be added properly. // // This example demonstrates how to create a Zipkin reporter using the standard // Zipkin http reporter // (https://godoc.org/github.com/openzipkin/zipkin-go/reporter/http) to send // Span data to New Relic. Follow this example when your application uses // Zipkin for tracing (instead of the New Relic Go Agent) and you wish to send // span data to the New Relic backend. The example assumes you have the // environment variable NEW_RELIC_API_KEY set to your New Relic Insights Insert // Key. // // import ( // zipkin "github.com/openzipkin/zipkin-go" // reporterhttp "github.com/openzipkin/zipkin-go/reporter/http" // ) // // func main() { // reporter := reporterhttp.NewReporter( // "https://trace-api.newrelic.com/trace/v1", // reporterhttp.RequestCallback(func(req *http.Request) { // req.Header.Add("X-Insert-Key", os.Getenv("NEW_RELIC_API_KEY")) // req.Header.Add("Data-Format", "zipkin") // req.Header.Add("Data-Format-Version", "2") // }), // ) // defer reporter.Close() // // // use the reporter to create a new tracer // zipkin.NewTracer(reporter) // } package nrb3 go-agent-3.42.0/v3/integrations/nrb3/nrb3_test.go000066400000000000000000000121461510742411500214410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrb3 import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestNewRoundTripperNil(t *testing.T) { rt := NewRoundTripper(nil) if orig := rt.(*b3Transport).original; orig != http.DefaultTransport { t.Error("original is not as expected:", orig) } } type roundTripperFn func(*http.Request) (*http.Response, error) func (fn roundTripperFn) RoundTrip(r *http.Request) (*http.Response, error) { return fn(r) } func TestRoundTripperNoTxn(t *testing.T) { app := integrationsupport.NewTestApp(nil, integrationsupport.DTEnabledCfgFn) txn := app.StartTransaction("test") var count int rt := NewRoundTripper(roundTripperFn(func(req *http.Request) (*http.Response, error) { count++ return &http.Response{ StatusCode: 200, }, nil })) client := &http.Client{Transport: rt} req, err := http.NewRequest("GET", "http://example.com", nil) if nil != err { t.Fatal(err) } _, err = client.Do(req) if nil != err { t.Fatal(err) } txn.End() if count != 1 { t.Error("incorrect call count to RoundTripper:", count) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/test", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/test", Scope: "", Forced: false, Data: nil}, }) } func TestRoundTripperWithTxnSampled(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(123) } app := integrationsupport.NewTestApp(replyfn, integrationsupport.DTEnabledCfgFn) txn := app.StartTransaction("test") var count int var sent *http.Request rt := NewRoundTripper(roundTripperFn(func(req *http.Request) (*http.Response, error) { count++ sent = req return &http.Response{ StatusCode: 200, }, nil })) rt.(*b3Transport).idGen = internal.NewTraceIDGenerator(456) client := &http.Client{Transport: rt} req, err := http.NewRequest("GET", "http://example.com", nil) if nil != err { t.Fatal(err) } req = newrelic.RequestWithTransactionContext(req, txn) _, err = client.Do(req) if nil != err { t.Fatal(err) } txn.End() if count != 1 { t.Error("incorrect call count to RoundTripper:", count) } // original request is not modified if hdr := req.Header.Get("X-B3-TraceId"); hdr != "" { t.Error("original request was modified, X-B3-TraceId header set:", hdr) } // b3 headers added if hdr := sent.Header.Get("X-B3-TraceId"); hdr != "f1405ced8b9968baf9109259515bf702" { t.Error("unexpected value for X-B3-TraceId header:", hdr) } if hdr := sent.Header.Get("X-B3-SpanId"); hdr != "2e6fb48a8d962779" { t.Error("unexpected value for X-B3-SpanId header:", hdr) } if hdr := sent.Header.Get("X-B3-ParentSpanId"); hdr != "5a291b00ff0f4b36" { t.Error("unexpected value for X-B3-ParentSpanId header:", hdr) } if hdr := sent.Header.Get("X-B3-Sampled"); hdr != "1" { t.Error("unexpected value for X-B3-Sampled header:", hdr) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: "OtherTransaction/Go/test", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/test", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/test", Scope: "", Forced: false, Data: nil}, }) } func TestRoundTripperWithTxnNotSampled(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleNothing() } app := integrationsupport.NewTestApp(replyfn, integrationsupport.DTEnabledCfgFn) txn := app.StartTransaction("test") var sent *http.Request rt := NewRoundTripper(roundTripperFn(func(req *http.Request) (*http.Response, error) { sent = req return &http.Response{ StatusCode: 200, }, nil })) client := &http.Client{Transport: rt} req, err := http.NewRequest("GET", "http://example.com", nil) if nil != err { t.Fatal(err) } req = newrelic.RequestWithTransactionContext(req, txn) _, err = client.Do(req) if nil != err { t.Fatal(err) } txn.End() if hdr := sent.Header.Get("X-B3-Sampled"); hdr != "0" { t.Error("unexpected value for X-B3-Sampled header:", hdr) } } go-agent-3.42.0/v3/integrations/nrconnect/000077500000000000000000000000001510742411500203305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/LICENSE.txt000066400000000000000000000264501510742411500221620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrconnect/README.md000066400000000000000000000006411510742411500216100ustar00rootroot00000000000000# v3/integrations/nrconnect [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrconnect?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrconnect) Package `nrconnect` instruments https://github.com/connectrpc/connect-go. ``` import "github.com/newrelic/go-agent/v3/integrations/nrconnect" ``` See the [example](example/) directory for a complete working example. go-agent-3.42.0/v3/integrations/nrconnect/example/000077500000000000000000000000001510742411500217635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/example/client/000077500000000000000000000000001510742411500232415ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/example/client/client.go000066400000000000000000000061331510742411500250510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "crypto/tls" "errors" "io" "log" "net" "net/http" "os" "time" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/newrelic" "golang.org/x/net/http2" "github.com/newrelic/go-agent/v3/integrations/nrconnect" "github.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleapp" "github.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleapp/sampleappconnect" ) func doUnaryUnary(ctx context.Context, client sampleappconnect.SampleApplicationClient) { resp, err := client.DoUnaryUnary(ctx, connect.NewRequest(&sampleapp.Message{Text: "Hello DoUnaryUnary"})) if err != nil { panic(err) } log.Println(resp.Msg.Text) } func doUnaryStream(ctx context.Context, client sampleappconnect.SampleApplicationClient) { stream, err := client.DoUnaryStream(ctx, connect.NewRequest(&sampleapp.Message{Text: "Hello DoUnaryStream"})) if err != nil { panic(err) } for stream.Receive() { msg := stream.Msg() log.Println(msg.Text) } if err := stream.Err(); err != nil { panic(err) } } func doStreamUnary(ctx context.Context, client sampleappconnect.SampleApplicationClient) { stream := client.DoStreamUnary(ctx) for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello DoStreamUnary"}); err != nil { panic(err) } } resp, err := stream.CloseAndReceive() if err != nil { panic(err) } log.Println(resp.Msg.Text) } func doStreamStream(ctx context.Context, client sampleappconnect.SampleApplicationClient) { stream := client.DoStreamStream(ctx) waitc := make(chan struct{}) go func() { for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { close(waitc) return } if err != nil { panic(err) } log.Println(msg.Text) } }() for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello DoStreamStream"}); err != nil { panic(err) } } if err := stream.CloseRequest(); err != nil { panic(err) } <-waitc } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Connect Client"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } defer app.Shutdown(10 * time.Second) app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("main") defer txn.End() client := sampleappconnect.NewSampleApplicationClient( newInsecureClient(), "http://localhost:8080", connect.WithInterceptors(nrconnect.Interceptor(app)), connect.WithGRPC(), ) ctx := newrelic.NewContext(context.Background(), txn) doUnaryUnary(ctx, client) doUnaryStream(ctx, client) doStreamUnary(ctx, client) doStreamStream(ctx, client) } // https://connectrpc.com/docs/go/common-errors/#client-missing-h2c-configuration func newInsecureClient() *http.Client { return &http.Client{ Transport: &http2.Transport{ AllowHTTP: true, DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { return net.Dial(network, addr) }, }, } } go-agent-3.42.0/v3/integrations/nrconnect/example/go.mod000066400000000000000000000006361510742411500230760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrconnect/example go 1.24 require ( connectrpc.com/connect v1.16.2 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrconnect v0.0.0 golang.org/x/net v0.38.0 google.golang.org/protobuf v1.34.2 ) replace github.com/newrelic/go-agent/v3/integrations/nrconnect => .. replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/nrconnect/example/sampleapp/000077500000000000000000000000001510742411500237455ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/example/sampleapp/sampleapp.pb.go000066400000000000000000000105241510742411500266600ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 // protoc (unknown) // source: sampleapp.proto package sampleapp import ( "reflect" "sync" "unsafe" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/runtime/protoimpl" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type Message struct { state protoimpl.MessageState `protogen:"open.v1"` Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Message) Reset() { *x = Message{} mi := &file_sampleapp_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Message) String() string { return protoimpl.X.MessageStringOf(x) } func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_sampleapp_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { return file_sampleapp_proto_rawDescGZIP(), []int{0} } func (x *Message) GetText() string { if x != nil { return x.Text } return "" } var File_sampleapp_proto protoreflect.FileDescriptor const file_sampleapp_proto_rawDesc = "" + "\n" + "\x0fsampleapp.proto\"\x1d\n" + "\aMessage\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text2\xb7\x01\n" + "\x11SampleApplication\x12$\n" + "\fDoUnaryUnary\x12\b.Message\x1a\b.Message\"\x00\x12'\n" + "\rDoUnaryStream\x12\b.Message\x1a\b.Message\"\x000\x01\x12'\n" + "\rDoStreamUnary\x12\b.Message\x1a\b.Message\"\x00(\x01\x12*\n" + "\x0eDoStreamStream\x12\b.Message\x1a\b.Message\"\x00(\x010\x01B\\B\x0eSampleappProtoP\x01ZHgithub.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleappb\x06proto3" var ( file_sampleapp_proto_rawDescOnce sync.Once file_sampleapp_proto_rawDescData []byte ) func file_sampleapp_proto_rawDescGZIP() []byte { file_sampleapp_proto_rawDescOnce.Do(func() { file_sampleapp_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sampleapp_proto_rawDesc), len(file_sampleapp_proto_rawDesc))) }) return file_sampleapp_proto_rawDescData } var file_sampleapp_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_sampleapp_proto_goTypes = []any{ (*Message)(nil), // 0: Message } var file_sampleapp_proto_depIdxs = []int32{ 0, // 0: SampleApplication.DoUnaryUnary:input_type -> Message 0, // 1: SampleApplication.DoUnaryStream:input_type -> Message 0, // 2: SampleApplication.DoStreamUnary:input_type -> Message 0, // 3: SampleApplication.DoStreamStream:input_type -> Message 0, // 4: SampleApplication.DoUnaryUnary:output_type -> Message 0, // 5: SampleApplication.DoUnaryStream:output_type -> Message 0, // 6: SampleApplication.DoStreamUnary:output_type -> Message 0, // 7: SampleApplication.DoStreamStream:output_type -> Message 4, // [4:8] is the sub-list for method output_type 0, // [0:4] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_sampleapp_proto_init() } func file_sampleapp_proto_init() { if File_sampleapp_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sampleapp_proto_rawDesc), len(file_sampleapp_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 1, }, GoTypes: file_sampleapp_proto_goTypes, DependencyIndexes: file_sampleapp_proto_depIdxs, MessageInfos: file_sampleapp_proto_msgTypes, }.Build() File_sampleapp_proto = out.File file_sampleapp_proto_goTypes = nil file_sampleapp_proto_depIdxs = nil } go-agent-3.42.0/v3/integrations/nrconnect/example/sampleapp/sampleapp.proto000066400000000000000000000006561510742411500270230ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; service SampleApplication { rpc DoUnaryUnary(Message) returns (Message) {} rpc DoUnaryStream(Message) returns (stream Message) {} rpc DoStreamUnary(stream Message) returns (Message) {} rpc DoStreamStream(stream Message) returns (stream Message) {} } message Message { string text = 1; } go-agent-3.42.0/v3/integrations/nrconnect/example/sampleapp/sampleappconnect/000077500000000000000000000000001510742411500273015ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/example/sampleapp/sampleappconnect/sampleapp.connect.go000066400000000000000000000235001510742411500332420ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: sampleapp.proto package sampleappconnect import ( "context" "errors" "net/http" "strings" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleapp" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // SampleApplicationName is the fully-qualified name of the SampleApplication service. SampleApplicationName = "SampleApplication" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // SampleApplicationDoUnaryUnaryProcedure is the fully-qualified name of the SampleApplication's // DoUnaryUnary RPC. SampleApplicationDoUnaryUnaryProcedure = "/SampleApplication/DoUnaryUnary" // SampleApplicationDoUnaryStreamProcedure is the fully-qualified name of the SampleApplication's // DoUnaryStream RPC. SampleApplicationDoUnaryStreamProcedure = "/SampleApplication/DoUnaryStream" // SampleApplicationDoStreamUnaryProcedure is the fully-qualified name of the SampleApplication's // DoStreamUnary RPC. SampleApplicationDoStreamUnaryProcedure = "/SampleApplication/DoStreamUnary" // SampleApplicationDoStreamStreamProcedure is the fully-qualified name of the SampleApplication's // DoStreamStream RPC. SampleApplicationDoStreamStreamProcedure = "/SampleApplication/DoStreamStream" ) // SampleApplicationClient is a client for the SampleApplication service. type SampleApplicationClient interface { DoUnaryUnary(context.Context, *connect.Request[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) DoUnaryStream(context.Context, *connect.Request[sampleapp.Message]) (*connect.ServerStreamForClient[sampleapp.Message], error) DoStreamUnary(context.Context) *connect.ClientStreamForClient[sampleapp.Message, sampleapp.Message] DoStreamStream(context.Context) *connect.BidiStreamForClient[sampleapp.Message, sampleapp.Message] } // NewSampleApplicationClient constructs a client for the SampleApplication service. By default, it // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or // connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewSampleApplicationClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SampleApplicationClient { baseURL = strings.TrimRight(baseURL, "/") sampleApplicationMethods := sampleapp.File_sampleapp_proto.Services().ByName("SampleApplication").Methods() return &sampleApplicationClient{ doUnaryUnary: connect.NewClient[sampleapp.Message, sampleapp.Message]( httpClient, baseURL+SampleApplicationDoUnaryUnaryProcedure, connect.WithSchema(sampleApplicationMethods.ByName("DoUnaryUnary")), connect.WithClientOptions(opts...), ), doUnaryStream: connect.NewClient[sampleapp.Message, sampleapp.Message]( httpClient, baseURL+SampleApplicationDoUnaryStreamProcedure, connect.WithSchema(sampleApplicationMethods.ByName("DoUnaryStream")), connect.WithClientOptions(opts...), ), doStreamUnary: connect.NewClient[sampleapp.Message, sampleapp.Message]( httpClient, baseURL+SampleApplicationDoStreamUnaryProcedure, connect.WithSchema(sampleApplicationMethods.ByName("DoStreamUnary")), connect.WithClientOptions(opts...), ), doStreamStream: connect.NewClient[sampleapp.Message, sampleapp.Message]( httpClient, baseURL+SampleApplicationDoStreamStreamProcedure, connect.WithSchema(sampleApplicationMethods.ByName("DoStreamStream")), connect.WithClientOptions(opts...), ), } } // sampleApplicationClient implements SampleApplicationClient. type sampleApplicationClient struct { doUnaryUnary *connect.Client[sampleapp.Message, sampleapp.Message] doUnaryStream *connect.Client[sampleapp.Message, sampleapp.Message] doStreamUnary *connect.Client[sampleapp.Message, sampleapp.Message] doStreamStream *connect.Client[sampleapp.Message, sampleapp.Message] } // DoUnaryUnary calls SampleApplication.DoUnaryUnary. func (c *sampleApplicationClient) DoUnaryUnary(ctx context.Context, req *connect.Request[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) { return c.doUnaryUnary.CallUnary(ctx, req) } // DoUnaryStream calls SampleApplication.DoUnaryStream. func (c *sampleApplicationClient) DoUnaryStream(ctx context.Context, req *connect.Request[sampleapp.Message]) (*connect.ServerStreamForClient[sampleapp.Message], error) { return c.doUnaryStream.CallServerStream(ctx, req) } // DoStreamUnary calls SampleApplication.DoStreamUnary. func (c *sampleApplicationClient) DoStreamUnary(ctx context.Context) *connect.ClientStreamForClient[sampleapp.Message, sampleapp.Message] { return c.doStreamUnary.CallClientStream(ctx) } // DoStreamStream calls SampleApplication.DoStreamStream. func (c *sampleApplicationClient) DoStreamStream(ctx context.Context) *connect.BidiStreamForClient[sampleapp.Message, sampleapp.Message] { return c.doStreamStream.CallBidiStream(ctx) } // SampleApplicationHandler is an implementation of the SampleApplication service. type SampleApplicationHandler interface { DoUnaryUnary(context.Context, *connect.Request[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) DoUnaryStream(context.Context, *connect.Request[sampleapp.Message], *connect.ServerStream[sampleapp.Message]) error DoStreamUnary(context.Context, *connect.ClientStream[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) DoStreamStream(context.Context, *connect.BidiStream[sampleapp.Message, sampleapp.Message]) error } // NewSampleApplicationHandler builds an HTTP handler from the service implementation. It returns // the path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewSampleApplicationHandler(svc SampleApplicationHandler, opts ...connect.HandlerOption) (string, http.Handler) { sampleApplicationMethods := sampleapp.File_sampleapp_proto.Services().ByName("SampleApplication").Methods() sampleApplicationDoUnaryUnaryHandler := connect.NewUnaryHandler( SampleApplicationDoUnaryUnaryProcedure, svc.DoUnaryUnary, connect.WithSchema(sampleApplicationMethods.ByName("DoUnaryUnary")), connect.WithHandlerOptions(opts...), ) sampleApplicationDoUnaryStreamHandler := connect.NewServerStreamHandler( SampleApplicationDoUnaryStreamProcedure, svc.DoUnaryStream, connect.WithSchema(sampleApplicationMethods.ByName("DoUnaryStream")), connect.WithHandlerOptions(opts...), ) sampleApplicationDoStreamUnaryHandler := connect.NewClientStreamHandler( SampleApplicationDoStreamUnaryProcedure, svc.DoStreamUnary, connect.WithSchema(sampleApplicationMethods.ByName("DoStreamUnary")), connect.WithHandlerOptions(opts...), ) sampleApplicationDoStreamStreamHandler := connect.NewBidiStreamHandler( SampleApplicationDoStreamStreamProcedure, svc.DoStreamStream, connect.WithSchema(sampleApplicationMethods.ByName("DoStreamStream")), connect.WithHandlerOptions(opts...), ) return "/SampleApplication/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case SampleApplicationDoUnaryUnaryProcedure: sampleApplicationDoUnaryUnaryHandler.ServeHTTP(w, r) case SampleApplicationDoUnaryStreamProcedure: sampleApplicationDoUnaryStreamHandler.ServeHTTP(w, r) case SampleApplicationDoStreamUnaryProcedure: sampleApplicationDoStreamUnaryHandler.ServeHTTP(w, r) case SampleApplicationDoStreamStreamProcedure: sampleApplicationDoStreamStreamHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedSampleApplicationHandler returns CodeUnimplemented from all methods. type UnimplementedSampleApplicationHandler struct{} func (UnimplementedSampleApplicationHandler) DoUnaryUnary(context.Context, *connect.Request[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("SampleApplication.DoUnaryUnary is not implemented")) } func (UnimplementedSampleApplicationHandler) DoUnaryStream(context.Context, *connect.Request[sampleapp.Message], *connect.ServerStream[sampleapp.Message]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("SampleApplication.DoUnaryStream is not implemented")) } func (UnimplementedSampleApplicationHandler) DoStreamUnary(context.Context, *connect.ClientStream[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("SampleApplication.DoStreamUnary is not implemented")) } func (UnimplementedSampleApplicationHandler) DoStreamStream(context.Context, *connect.BidiStream[sampleapp.Message, sampleapp.Message]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("SampleApplication.DoStreamStream is not implemented")) } go-agent-3.42.0/v3/integrations/nrconnect/example/server/000077500000000000000000000000001510742411500232715ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/example/server/server.go000066400000000000000000000052041510742411500251270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "io" "log" "net/http" "os" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/newrelic" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "github.com/newrelic/go-agent/v3/integrations/nrconnect" "github.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleapp" "github.com/newrelic/go-agent/v3/integrations/nrconnect/example/sampleapp/sampleappconnect" ) type service struct{} func processMessage(ctx context.Context, msg *sampleapp.Message) { defer newrelic.FromContext(ctx).StartSegment("processMessage").End() log.Printf("Message received: %s\n", msg.Text) } func (s *service) DoUnaryUnary(ctx context.Context, req *connect.Request[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) { processMessage(ctx, req.Msg) return connect.NewResponse(&sampleapp.Message{Text: "Hello from DoUnaryUnary"}), nil } func (s *service) DoUnaryStream(ctx context.Context, req *connect.Request[sampleapp.Message], stream *connect.ServerStream[sampleapp.Message]) error { processMessage(ctx, req.Msg) for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello from DoUnaryStream"}); err != nil { return err } } return nil } func (s *service) DoStreamUnary(ctx context.Context, stream *connect.ClientStream[sampleapp.Message]) (*connect.Response[sampleapp.Message], error) { for stream.Receive() { msg := stream.Msg() processMessage(ctx, msg) } if err := stream.Err(); err != nil { return nil, err } return connect.NewResponse(&sampleapp.Message{Text: "Hello from DoStreamUnary"}), nil } func (s *service) DoStreamStream(ctx context.Context, stream *connect.BidiStream[sampleapp.Message, sampleapp.Message]) error { for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { return nil } if err != nil { return err } processMessage(ctx, msg) if err := stream.Send(&sampleapp.Message{Text: "Hello from DoStreamStream"}); err != nil { return err } } } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Connect service"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // Create the Connect handler with New Relic instrumentation mux := http.NewServeMux() mux.Handle(sampleappconnect.NewSampleApplicationHandler( &service{}, connect.WithInterceptors(nrconnect.Interceptor(app)), )) server := &http.Server{ Addr: ":8080", Handler: h2c.NewHandler(mux, &http2.Server{}), } server.ListenAndServe() } go-agent-3.42.0/v3/integrations/nrconnect/go.mod000066400000000000000000000003641510742411500214410ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrconnect go 1.24 require ( connectrpc.com/connect v1.16.2 github.com/newrelic/go-agent/v3 v3.42.0 google.golang.org/protobuf v1.34.2 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrconnect/nrconnect.go000066400000000000000000000200121510742411500226430ustar00rootroot00000000000000// Copyright 2024 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // This integration instruments Connect RPC service calls via interceptor functions. // // The results of these calls are reported as errors or as informational // messages based on the Connect status code they return. // // In the simplest case, simply add an interceptor when creating your handler or client: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("Connect Server"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDebugLogger(os.Stdout), // ) // // // For server handlers: // mux := http.NewServeMux() // path, handler := greetv1connect.NewGreetServiceHandler( // &greetServer{}, // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) // mux.Handle(path, handler) // // // For clients: // client := greetv1connect.NewGreetServiceClient( // http.DefaultClient, // "https://api.acme.com", // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) // package nrconnect import ( "context" "errors" "net/http" "net/url" "strings" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/newrelic" ) func getURL(method, target string) *url.URL { return &url.URL{ Scheme: "connect", Host: target, Path: method, } } // startTransaction starts a New Relic transaction for server-side requests func startTransaction(ctx context.Context, app *newrelic.Application, method string, hdr http.Header) *newrelic.Transaction { method = strings.TrimPrefix(method, "/") target := hdr.Get("Host") url := getURL(method, target) transport := newrelic.TransportHTTP webReq := newrelic.WebRequest{ Header: hdr, URL: url, Method: method, Transport: transport, } txn := app.StartTransaction(method) txn.SetWebRequest(webReq) return txn } // startClientSegment starts an ExternalSegment for client-side requests and adds Distributed Trace headers func startClientSegment(ctx context.Context, method, target string, hdr http.Header) *newrelic.ExternalSegment { txn := newrelic.FromContext(ctx) if txn == nil { return nil } method = strings.TrimPrefix(method, "/") seg := newrelic.StartExternalSegment(txn, getDummyRequest(method, target, hdr)) seg.Host = getURL(method, target).Host seg.Library = "Connect" seg.Procedure = method if hdr != nil { setTracingHeaders(ctx, hdr) } return seg } func getDummyRequest(method, target string, hdr http.Header) *http.Request { return &http.Request{ Method: "POST", URL: getURL(method, target), Header: hdr, } } // reportStatus reports the Connect error status to New Relic func reportStatus(ctx context.Context, txn *newrelic.Transaction, err error) { if err == nil { txn.SetWebResponse(nil).WriteHeader(200) return } // Handle Connect errors if connectErr := new(connect.Error); errors.As(err, &connectErr) { code := connectErr.Code() message := connectErr.Message() // Set HTTP status based on Connect code // https://connectrpc.com/docs/protocol#error-codes httpStatus := 200 switch code { case connect.CodeInvalidArgument, connect.CodeOutOfRange: httpStatus = 400 case connect.CodeUnauthenticated: httpStatus = 401 case connect.CodePermissionDenied: httpStatus = 403 case connect.CodeNotFound: httpStatus = 404 case connect.CodeResourceExhausted: httpStatus = 429 case connect.CodeCanceled: httpStatus = 499 case connect.CodeInternal, connect.CodeDataLoss: httpStatus = 500 case connect.CodeUnimplemented: httpStatus = 501 case connect.CodeUnavailable: httpStatus = 503 case connect.CodeDeadlineExceeded: httpStatus = 504 } txn.SetWebResponse(nil).WriteHeader(httpStatus) // Report as error for serious status codes if code == connect.CodeInternal || code == connect.CodeDataLoss || code == connect.CodeUnknown { txn.NoticeError(&newrelic.Error{ Message: message, Class: "Connect Error: " + code.String(), }) } txn.AddAttribute("connectStatusCode", code.String()) txn.AddAttribute("connectStatusMessage", message) return } // Non-Connect error txn.SetWebResponse(nil).WriteHeader(500) txn.NoticeError(err) } // Interceptor creates a Connect interceptor that instruments RPC calls with New Relic monitoring. // // This interceptor automatically creates transactions for server-side handlers and external segments // for client-side calls. It also adds distributed tracing headers for client requests. // // Example usage: // // app, _ := newrelic.NewApplication(...) // // // For server handlers: // mux := http.NewServeMux() // path, handler := greetv1connect.NewGreetServiceHandler( // &greetServer{}, // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) // mux.Handle(path, handler) // // // For clients: // client := greetv1connect.NewGreetServiceClient( // http.DefaultClient, // "https://api.acme.com", // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) func Interceptor(app *newrelic.Application) connect.Interceptor { return &interceptor{app: app} } type interceptor struct { app *newrelic.Application } func (i *interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { if i.app == nil { return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { return next(ctx, request) } } return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { method := req.Spec().Procedure if req.Spec().IsClient { // Client-side: create external segment seg := startClientSegment(ctx, method, req.Peer().Addr, req.Header()) if seg != nil { defer seg.End() } return next(ctx, req) } else { // Server-side: create transaction txn := startTransaction(ctx, i.app, method, req.Header()) defer func() { txn.End() }() ctx = newrelic.NewContext(ctx, txn) resp, err := next(ctx, req) reportStatus(ctx, txn, err) return resp, err } } } func (i *interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { if i.app == nil { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { return next(ctx, spec) } } return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { conn := next(ctx, spec) method := spec.Procedure target := conn.Peer().Addr seg := startClientSegment(ctx, method, target, conn.RequestHeader()) return &wrappedStreamingClientConn{ StreamingClientConn: conn, segment: seg, ended: false, } } } func (i *interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { if i.app == nil { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { return next(ctx, conn) } } return func(ctx context.Context, conn connect.StreamingHandlerConn) error { method := conn.Spec().Procedure txn := startTransaction(ctx, i.app, method, conn.RequestHeader()) defer txn.End() ctx = newrelic.NewContext(ctx, txn) err := next(ctx, conn) reportStatus(ctx, txn, err) return err } } type wrappedStreamingClientConn struct { connect.StreamingClientConn segment *newrelic.ExternalSegment ended bool } func (c *wrappedStreamingClientConn) Receive(m any) error { err := c.StreamingClientConn.Receive(m) if err != nil && !c.ended { if c.segment != nil { c.segment.End() c.ended = true } } return err } func (c *wrappedStreamingClientConn) CloseRequest() error { err := c.StreamingClientConn.CloseRequest() if !c.ended { if c.segment != nil { c.segment.End() c.ended = true } } return err } func (c *wrappedStreamingClientConn) CloseResponse() error { err := c.StreamingClientConn.CloseResponse() if !c.ended { if c.segment != nil { c.segment.End() c.ended = true } } return err } func setTracingHeaders(ctx context.Context, target http.Header) { tracingHeaders := http.Header{} txn := newrelic.FromContext(ctx) if txn != nil { txn.InsertDistributedTraceHeaders(tracingHeaders) for k, values := range tracingHeaders { for _, v := range values { target.Set(k, v) } } } } go-agent-3.42.0/v3/integrations/nrconnect/nrconnect_doc.go000066400000000000000000000072231510742411500235010ustar00rootroot00000000000000// Copyright 2024 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrconnect instruments https://github.com/connectrpc/connect-go. // // This package can be used to instrument Connect RPC servers and Connect RPC clients. // // # Server // // To instrument a Connect RPC server, use the Interceptor function with your // newrelic.Application to create an interceptor to pass to connect.WithInterceptors // when creating your handler. // // The results of these calls are reported as errors or as informational // messages based on the Connect status code they return. // // In the simplest case, simply add an interceptor when creating your handler: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("Connect Server"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDebugLogger(os.Stdout), // ) // // mux := http.NewServeMux() // path, handler := greetv1connect.NewGreetServiceHandler( // &greetServer{}, // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) // mux.Handle(path, handler) // // The disposition of each Connect status code is handled as follows: // // OK 200 HTTP status, no error reported // InvalidArgument 400 HTTP status // OutOfRange 400 HTTP status // Unauthenticated 401 HTTP status // PermissionDenied 403 HTTP status // NotFound 404 HTTP status // ResourceExhausted 429 HTTP status // Canceled 499 HTTP status // Internal 500 HTTP status, reported as error // DataLoss 500 HTTP status, reported as error // Unknown 500 HTTP status, reported as error // Unimplemented 501 HTTP status // Unavailable 503 HTTP status // DeadlineExceeded 504 HTTP status // // These interceptors create transactions for inbound calls. The transaction is // added to the call context and can be accessed in your method handlers // using newrelic.FromContext. // // // handler is your Connect RPC server handler. Access the currently running // // transaction using newrelic.FromContext. // func (s *Server) Greet(ctx context.Context, req *connect.Request[greetv1.GreetRequest]) (*connect.Response[greetv1.GreetResponse], error) { // if err := processRequest(req); err != nil { // txn := newrelic.FromContext(ctx) // txn.NoticeError(err) // return nil, err // } // return connect.NewResponse(&greetv1.GreetResponse{Greeting: "Hello World!"}), nil // } // // Full server example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrconnect/example/server/server.go // // # Client // // To instrument a Connect RPC client, use the Interceptor function when creating a // Connect RPC client. Example: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("Connect Client"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // ) // // client := greetv1connect.NewGreetServiceClient( // http.DefaultClient, // "https://api.acme.com", // connect.WithInterceptors(nrconnect.Interceptor(app)), // ) // // Ensure that calls made with this Connect RPC client are done with a context // which contains a newrelic.Transaction. // // // Add the currently running transaction to the context before making a // // client call. // ctx := newrelic.NewContext(context.Background(), txn) // resp, err := client.Greet(ctx, connect.NewRequest(&greetv1.GreetRequest{Name: "World"})) // // Full client example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrconnect/example/client/client.go package nrconnect import "github.com/newrelic/go-agent/v3/internal" func init() { internal.TrackUsage("integration", "framework", "connect") } go-agent-3.42.0/v3/integrations/nrconnect/nrconnect_test.go000066400000000000000000000432731510742411500237200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrconnect import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/newrelic/go-agent/v3/integrations/nrconnect/testapp" "github.com/newrelic/go-agent/v3/integrations/nrconnect/testapp/testappconnect" ) func TestGetURL(t *testing.T) { testcases := []struct { method string target string expected string }{ { method: "/TestApplication/DoUnaryUnary", target: "", expected: "connect:///TestApplication/DoUnaryUnary", }, { method: "TestApplication/DoUnaryUnary", target: "", expected: "connect://TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: "localhost:8080", expected: "connect://localhost:8080/TestApplication/DoUnaryUnary", }, { method: "TestApplication/DoUnaryUnary", target: "localhost:8080", expected: "connect://localhost:8080/TestApplication/DoUnaryUnary", }, } for _, test := range testcases { actual := getURL(test.method, test.target) if actual.String() != test.expected { t.Errorf("incorrect URL:\n\tmethod=%s,\n\ttarget=%s,\n\texpected=%s,\n\tactual=%s", test.method, test.target, test.expected, actual.String()) } } } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.AccountID = "123" reply.TrustedAccountKey = "123" reply.PrimaryAppID = "456" } func testApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } func newTestServerAndConn(t *testing.T, app *newrelic.Application) (*httptest.Server, *http.Client) { t.Helper() mux := http.NewServeMux() mux.Handle(testappconnect.NewTestApplicationHandler(&testapp.Server{}, connect.WithInterceptors(Interceptor(app)))) sv := httptest.NewServer(mux) t.Cleanup(sv.Close) return sv, sv.Client() } func TestUnaryClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("UnaryUnary") ctx := newrelic.NewContext(context.Background(), txn) sv, client := newTestServerAndConn(t, nil) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL, connect.WithInterceptors(Interceptor(app.Application))) resp, err := connectClient.DoUnaryUnary(ctx, connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["Newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/UnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/UnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/" + sv.Listener.Addr().String() + "/all", Scope: "", Forced: false, Data: nil}, {Name: "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryUnary", Scope: "OtherTransaction/Go/UnaryUnary", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "Connect", "name": "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryUnary", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/UnaryUnary", "transaction.name": "OtherTransaction/Go/UnaryUnary", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/UnaryUnary", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/UnaryUnary", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryUnary", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestUnaryStreamClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("UnaryStream") ctx := newrelic.NewContext(context.Background(), txn) sv, client := newTestServerAndConn(t, nil) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL, connect.WithInterceptors(Interceptor(app.Application))) stream, err := connectClient.DoUnaryStream(ctx, connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int for stream.Receive() { msg := stream.Msg() var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["Newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } recved++ } if err := stream.Err(); err != nil { t.Fatal("error receiving message", err) } if recved != 3 { t.Fatal("received incorrect number of messages from server", recved) } txn.End() // In Connect RPC, DoUnaryStream is handled as a single HTTP request with streaming response // So it's treated as a unary call from the interceptor perspective app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/UnaryStream", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/UnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/" + sv.Listener.Addr().String() + "/all", Scope: "", Forced: false, Data: nil}, {Name: "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryStream", Scope: "OtherTransaction/Go/UnaryStream", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "Connect", "name": "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryStream", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/UnaryStream", "transaction.name": "OtherTransaction/Go/UnaryStream", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/UnaryStream", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/UnaryStream", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/" + sv.Listener.Addr().String() + "/Connect/TestApplication/DoUnaryStream", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestNilTxnClientUnary(t *testing.T) { sv, client := newTestServerAndConn(t, nil) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL) resp, err := connectClient.DoUnaryUnary(context.Background(), connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if _, ok := hdrs["Newrelic"]; ok { t.Error("distributed trace header sent", hdrs) } } func TestUnaryServerInterceptor(t *testing.T) { app := testApp() sv, client := newTestServerAndConn(t, app.Application) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL, connect.WithInterceptors(Interceptor(app.Application))) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) _, err := connectClient.DoUnaryUnary(ctx, connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "WebTransaction/Go/TestApplication/DoUnaryUnary", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 200, "http.statusCode": 200, "request.headers.contentType": "application/proto", "request.headers.contentLength": 0, "request.method": "TestApplication/DoUnaryUnary", "request.uri": "connect://" + sv.Listener.Addr().String() + "/TestApplication/DoUnaryUnary", }, }}) // Span events validation simplified to focus on basic metrics } func TestUnaryServerInterceptorError(t *testing.T) { app := testApp() sv, client := newTestServerAndConn(t, app.Application) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL) _, err := connectClient.DoUnaryUnaryError(context.Background(), connect.NewRequest(&testapp.Message{})) if err == nil { t.Fatal("DoUnaryUnaryError should have returned an error") } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Errors/WebTransaction/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", "nr.apdexPerfZone": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "connectStatusCode": "data_loss", "connectStatusMessage": "", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 500, "http.statusCode": 500, "request.headers.contentType": "application/proto", "request.headers.contentLength": 0, "request.method": "TestApplication/DoUnaryUnaryError", "request.uri": "connect://" + sv.Listener.Addr().String() + "/TestApplication/DoUnaryUnaryError", }, }}) // Error events are expected but validation is simplified } func TestUnaryStreamServerInterceptor(t *testing.T) { app := testApp() sv, client := newTestServerAndConn(t, app.Application) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL, connect.WithInterceptors(Interceptor(app.Application))) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := connectClient.DoUnaryStream(ctx, connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int for stream.Receive() { recved++ } if err := stream.Err(); err != nil { t.Fatal("error receiving message", err) } if recved != 3 { t.Fatal("received incorrect number of messages from server", recved) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryStream", Scope: "WebTransaction/Go/TestApplication/DoUnaryStream", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryStream", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryStream", Scope: "", Forced: false, Data: nil}, }) // Basic validation that transaction and span events are generated } func TestUnaryServerInterceptorNilApp(t *testing.T) { sv, client := newTestServerAndConn(t, nil) defer sv.Close() connectClient := testappconnect.NewTestApplicationClient(client, sv.URL) resp, err := connectClient.DoUnaryUnary(context.Background(), connect.NewRequest(&testapp.Message{})) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } if resp.Msg.Text == "" { t.Error("incorrect message received") } } go-agent-3.42.0/v3/integrations/nrconnect/testapp/000077500000000000000000000000001510742411500220105ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/testapp/buf.gen.yaml000066400000000000000000000005751510742411500242270ustar00rootroot00000000000000version: v2 managed: enabled: true override: - file_option: go_package_prefix value: github.com/newrelic/go-agent/v3/integrations/nrconnect/testapp plugins: - remote: buf.build/connectrpc/go:v1.18.1 out: . opt: - paths=source_relative # dependencies - remote: buf.build/protocolbuffers/go:v1.36.6 out: . opt: - paths=source_relative go-agent-3.42.0/v3/integrations/nrconnect/testapp/buf.yaml000066400000000000000000000001311510742411500234430ustar00rootroot00000000000000version: v2 lint: use: - STANDARD breaking: use: - FILE modules: - path: . go-agent-3.42.0/v3/integrations/nrconnect/testapp/server.go000066400000000000000000000052431510742411500236510ustar00rootroot00000000000000// Copyright 2024 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package testapp import ( "context" "encoding/json" "errors" "io" "connectrpc.com/connect" "github.com/newrelic/go-agent/v3/newrelic" ) // Server is a Connect RPC server. type Server struct{} // DoUnaryUnary is a unary request, unary response method. func (s *Server) DoUnaryUnary(ctx context.Context, req *connect.Request[Message]) (*connect.Response[Message], error) { defer newrelic.FromContext(ctx).StartSegment("DoUnaryUnary").End() headers := req.Header() js, _ := json.Marshal(headers) response := connect.NewResponse(&Message{Text: string(js)}) return response, nil } // DoUnaryStream is a unary request, stream response method. func (s *Server) DoUnaryStream(ctx context.Context, req *connect.Request[Message], stream *connect.ServerStream[Message]) error { defer newrelic.FromContext(ctx).StartSegment("DoUnaryStream").End() headers := req.Header() js, _ := json.Marshal(headers) for i := 0; i < 3; i++ { if err := stream.Send(&Message{Text: string(js)}); err != nil { return err } } return nil } // DoStreamUnary is a stream request, unary response method. func (s *Server) DoStreamUnary(ctx context.Context, stream *connect.ClientStream[Message]) (*connect.Response[Message], error) { defer newrelic.FromContext(ctx).StartSegment("DoStreamUnary").End() headers := stream.RequestHeader() js, _ := json.Marshal(headers) for stream.Receive() { } err := stream.Err() if errors.Is(err, io.EOF) { response := connect.NewResponse(&Message{Text: string(js)}) return response, nil } return nil, err } // DoStreamStream is a stream request, stream response method. func (s *Server) DoStreamStream(ctx context.Context, stream *connect.BidiStream[Message, Message]) error { defer newrelic.FromContext(ctx).StartSegment("DoStreamStream").End() headers := stream.RequestHeader() js, _ := json.Marshal(headers) for { _, err := stream.Receive() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } if err := stream.Send(&Message{Text: string(js)}); err != nil { return err } } } // DoUnaryUnaryError is a unary request, unary response method that returns an // error. func (s *Server) DoUnaryUnaryError(ctx context.Context, req *connect.Request[Message]) (*connect.Response[Message], error) { return nil, connect.NewError(connect.CodeDataLoss, nil) } // DoUnaryStreamError is a unary request, stream response method that returns an // error. func (s *Server) DoUnaryStreamError(ctx context.Context, req *connect.Request[Message], stream *connect.ServerStream[Message]) error { return connect.NewError(connect.CodeDataLoss, nil) } go-agent-3.42.0/v3/integrations/nrconnect/testapp/testapp.pb.go000066400000000000000000000112741510742411500244240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 // protoc (unknown) // source: testapp.proto package testapp import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type Message struct { state protoimpl.MessageState `protogen:"open.v1"` Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Message) Reset() { *x = Message{} mi := &file_testapp_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Message) String() string { return protoimpl.X.MessageStringOf(x) } func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_testapp_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { return file_testapp_proto_rawDescGZIP(), []int{0} } func (x *Message) GetText() string { if x != nil { return x.Text } return "" } var File_testapp_proto protoreflect.FileDescriptor const file_testapp_proto_rawDesc = "" + "\n" + "\rtestapp.proto\"\x1d\n" + "\aMessage\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text2\x8e\x02\n" + "\x0fTestApplication\x12$\n" + "\fDoUnaryUnary\x12\b.Message\x1a\b.Message\"\x00\x12'\n" + "\rDoUnaryStream\x12\b.Message\x1a\b.Message\"\x000\x01\x12'\n" + "\rDoStreamUnary\x12\b.Message\x1a\b.Message\"\x00(\x01\x12*\n" + "\x0eDoStreamStream\x12\b.Message\x1a\b.Message\"\x00(\x010\x01\x12)\n" + "\x11DoUnaryUnaryError\x12\b.Message\x1a\b.Message\"\x00\x12,\n" + "\x12DoUnaryStreamError\x12\b.Message\x1a\b.Message\"\x000\x01BPB\fTestappProtoP\x01Z>github.com/newrelic/go-agent/v3/integrations/nrconnect/testappb\x06proto3" var ( file_testapp_proto_rawDescOnce sync.Once file_testapp_proto_rawDescData []byte ) func file_testapp_proto_rawDescGZIP() []byte { file_testapp_proto_rawDescOnce.Do(func() { file_testapp_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_testapp_proto_rawDesc), len(file_testapp_proto_rawDesc))) }) return file_testapp_proto_rawDescData } var file_testapp_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_testapp_proto_goTypes = []any{ (*Message)(nil), // 0: Message } var file_testapp_proto_depIdxs = []int32{ 0, // 0: TestApplication.DoUnaryUnary:input_type -> Message 0, // 1: TestApplication.DoUnaryStream:input_type -> Message 0, // 2: TestApplication.DoStreamUnary:input_type -> Message 0, // 3: TestApplication.DoStreamStream:input_type -> Message 0, // 4: TestApplication.DoUnaryUnaryError:input_type -> Message 0, // 5: TestApplication.DoUnaryStreamError:input_type -> Message 0, // 6: TestApplication.DoUnaryUnary:output_type -> Message 0, // 7: TestApplication.DoUnaryStream:output_type -> Message 0, // 8: TestApplication.DoStreamUnary:output_type -> Message 0, // 9: TestApplication.DoStreamStream:output_type -> Message 0, // 10: TestApplication.DoUnaryUnaryError:output_type -> Message 0, // 11: TestApplication.DoUnaryStreamError:output_type -> Message 6, // [6:12] is the sub-list for method output_type 0, // [0:6] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_testapp_proto_init() } func file_testapp_proto_init() { if File_testapp_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_testapp_proto_rawDesc), len(file_testapp_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 1, }, GoTypes: file_testapp_proto_goTypes, DependencyIndexes: file_testapp_proto_depIdxs, MessageInfos: file_testapp_proto_msgTypes, }.Build() File_testapp_proto = out.File file_testapp_proto_goTypes = nil file_testapp_proto_depIdxs = nil } go-agent-3.42.0/v3/integrations/nrconnect/testapp/testapp.proto000066400000000000000000000010451510742411500245550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; service TestApplication { rpc DoUnaryUnary(Message) returns (Message) {} rpc DoUnaryStream(Message) returns (stream Message) {} rpc DoStreamUnary(stream Message) returns (Message) {} rpc DoStreamStream(stream Message) returns (stream Message) {} rpc DoUnaryUnaryError(Message) returns (Message) {} rpc DoUnaryStreamError(Message) returns (stream Message) {} } message Message { string text = 1; } go-agent-3.42.0/v3/integrations/nrconnect/testapp/testappconnect/000077500000000000000000000000001510742411500250425ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrconnect/testapp/testappconnect/testapp.connect.go000066400000000000000000000317041510742411500305060ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: testapp.proto package testappconnect import ( connect "connectrpc.com/connect" context "context" errors "errors" testapp "github.com/newrelic/go-agent/v3/integrations/nrconnect/testapp" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // TestApplicationName is the fully-qualified name of the TestApplication service. TestApplicationName = "TestApplication" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // TestApplicationDoUnaryUnaryProcedure is the fully-qualified name of the TestApplication's // DoUnaryUnary RPC. TestApplicationDoUnaryUnaryProcedure = "/TestApplication/DoUnaryUnary" // TestApplicationDoUnaryStreamProcedure is the fully-qualified name of the TestApplication's // DoUnaryStream RPC. TestApplicationDoUnaryStreamProcedure = "/TestApplication/DoUnaryStream" // TestApplicationDoStreamUnaryProcedure is the fully-qualified name of the TestApplication's // DoStreamUnary RPC. TestApplicationDoStreamUnaryProcedure = "/TestApplication/DoStreamUnary" // TestApplicationDoStreamStreamProcedure is the fully-qualified name of the TestApplication's // DoStreamStream RPC. TestApplicationDoStreamStreamProcedure = "/TestApplication/DoStreamStream" // TestApplicationDoUnaryUnaryErrorProcedure is the fully-qualified name of the TestApplication's // DoUnaryUnaryError RPC. TestApplicationDoUnaryUnaryErrorProcedure = "/TestApplication/DoUnaryUnaryError" // TestApplicationDoUnaryStreamErrorProcedure is the fully-qualified name of the TestApplication's // DoUnaryStreamError RPC. TestApplicationDoUnaryStreamErrorProcedure = "/TestApplication/DoUnaryStreamError" ) // TestApplicationClient is a client for the TestApplication service. type TestApplicationClient interface { DoUnaryUnary(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) DoUnaryStream(context.Context, *connect.Request[testapp.Message]) (*connect.ServerStreamForClient[testapp.Message], error) DoStreamUnary(context.Context) *connect.ClientStreamForClient[testapp.Message, testapp.Message] DoStreamStream(context.Context) *connect.BidiStreamForClient[testapp.Message, testapp.Message] DoUnaryUnaryError(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) DoUnaryStreamError(context.Context, *connect.Request[testapp.Message]) (*connect.ServerStreamForClient[testapp.Message], error) } // NewTestApplicationClient constructs a client for the TestApplication service. By default, it uses // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or // connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewTestApplicationClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) TestApplicationClient { baseURL = strings.TrimRight(baseURL, "/") testApplicationMethods := testapp.File_testapp_proto.Services().ByName("TestApplication").Methods() return &testApplicationClient{ doUnaryUnary: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoUnaryUnaryProcedure, connect.WithSchema(testApplicationMethods.ByName("DoUnaryUnary")), connect.WithClientOptions(opts...), ), doUnaryStream: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoUnaryStreamProcedure, connect.WithSchema(testApplicationMethods.ByName("DoUnaryStream")), connect.WithClientOptions(opts...), ), doStreamUnary: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoStreamUnaryProcedure, connect.WithSchema(testApplicationMethods.ByName("DoStreamUnary")), connect.WithClientOptions(opts...), ), doStreamStream: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoStreamStreamProcedure, connect.WithSchema(testApplicationMethods.ByName("DoStreamStream")), connect.WithClientOptions(opts...), ), doUnaryUnaryError: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoUnaryUnaryErrorProcedure, connect.WithSchema(testApplicationMethods.ByName("DoUnaryUnaryError")), connect.WithClientOptions(opts...), ), doUnaryStreamError: connect.NewClient[testapp.Message, testapp.Message]( httpClient, baseURL+TestApplicationDoUnaryStreamErrorProcedure, connect.WithSchema(testApplicationMethods.ByName("DoUnaryStreamError")), connect.WithClientOptions(opts...), ), } } // testApplicationClient implements TestApplicationClient. type testApplicationClient struct { doUnaryUnary *connect.Client[testapp.Message, testapp.Message] doUnaryStream *connect.Client[testapp.Message, testapp.Message] doStreamUnary *connect.Client[testapp.Message, testapp.Message] doStreamStream *connect.Client[testapp.Message, testapp.Message] doUnaryUnaryError *connect.Client[testapp.Message, testapp.Message] doUnaryStreamError *connect.Client[testapp.Message, testapp.Message] } // DoUnaryUnary calls TestApplication.DoUnaryUnary. func (c *testApplicationClient) DoUnaryUnary(ctx context.Context, req *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) { return c.doUnaryUnary.CallUnary(ctx, req) } // DoUnaryStream calls TestApplication.DoUnaryStream. func (c *testApplicationClient) DoUnaryStream(ctx context.Context, req *connect.Request[testapp.Message]) (*connect.ServerStreamForClient[testapp.Message], error) { return c.doUnaryStream.CallServerStream(ctx, req) } // DoStreamUnary calls TestApplication.DoStreamUnary. func (c *testApplicationClient) DoStreamUnary(ctx context.Context) *connect.ClientStreamForClient[testapp.Message, testapp.Message] { return c.doStreamUnary.CallClientStream(ctx) } // DoStreamStream calls TestApplication.DoStreamStream. func (c *testApplicationClient) DoStreamStream(ctx context.Context) *connect.BidiStreamForClient[testapp.Message, testapp.Message] { return c.doStreamStream.CallBidiStream(ctx) } // DoUnaryUnaryError calls TestApplication.DoUnaryUnaryError. func (c *testApplicationClient) DoUnaryUnaryError(ctx context.Context, req *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) { return c.doUnaryUnaryError.CallUnary(ctx, req) } // DoUnaryStreamError calls TestApplication.DoUnaryStreamError. func (c *testApplicationClient) DoUnaryStreamError(ctx context.Context, req *connect.Request[testapp.Message]) (*connect.ServerStreamForClient[testapp.Message], error) { return c.doUnaryStreamError.CallServerStream(ctx, req) } // TestApplicationHandler is an implementation of the TestApplication service. type TestApplicationHandler interface { DoUnaryUnary(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) DoUnaryStream(context.Context, *connect.Request[testapp.Message], *connect.ServerStream[testapp.Message]) error DoStreamUnary(context.Context, *connect.ClientStream[testapp.Message]) (*connect.Response[testapp.Message], error) DoStreamStream(context.Context, *connect.BidiStream[testapp.Message, testapp.Message]) error DoUnaryUnaryError(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) DoUnaryStreamError(context.Context, *connect.Request[testapp.Message], *connect.ServerStream[testapp.Message]) error } // NewTestApplicationHandler builds an HTTP handler from the service implementation. It returns the // path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewTestApplicationHandler(svc TestApplicationHandler, opts ...connect.HandlerOption) (string, http.Handler) { testApplicationMethods := testapp.File_testapp_proto.Services().ByName("TestApplication").Methods() testApplicationDoUnaryUnaryHandler := connect.NewUnaryHandler( TestApplicationDoUnaryUnaryProcedure, svc.DoUnaryUnary, connect.WithSchema(testApplicationMethods.ByName("DoUnaryUnary")), connect.WithHandlerOptions(opts...), ) testApplicationDoUnaryStreamHandler := connect.NewServerStreamHandler( TestApplicationDoUnaryStreamProcedure, svc.DoUnaryStream, connect.WithSchema(testApplicationMethods.ByName("DoUnaryStream")), connect.WithHandlerOptions(opts...), ) testApplicationDoStreamUnaryHandler := connect.NewClientStreamHandler( TestApplicationDoStreamUnaryProcedure, svc.DoStreamUnary, connect.WithSchema(testApplicationMethods.ByName("DoStreamUnary")), connect.WithHandlerOptions(opts...), ) testApplicationDoStreamStreamHandler := connect.NewBidiStreamHandler( TestApplicationDoStreamStreamProcedure, svc.DoStreamStream, connect.WithSchema(testApplicationMethods.ByName("DoStreamStream")), connect.WithHandlerOptions(opts...), ) testApplicationDoUnaryUnaryErrorHandler := connect.NewUnaryHandler( TestApplicationDoUnaryUnaryErrorProcedure, svc.DoUnaryUnaryError, connect.WithSchema(testApplicationMethods.ByName("DoUnaryUnaryError")), connect.WithHandlerOptions(opts...), ) testApplicationDoUnaryStreamErrorHandler := connect.NewServerStreamHandler( TestApplicationDoUnaryStreamErrorProcedure, svc.DoUnaryStreamError, connect.WithSchema(testApplicationMethods.ByName("DoUnaryStreamError")), connect.WithHandlerOptions(opts...), ) return "/TestApplication/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case TestApplicationDoUnaryUnaryProcedure: testApplicationDoUnaryUnaryHandler.ServeHTTP(w, r) case TestApplicationDoUnaryStreamProcedure: testApplicationDoUnaryStreamHandler.ServeHTTP(w, r) case TestApplicationDoStreamUnaryProcedure: testApplicationDoStreamUnaryHandler.ServeHTTP(w, r) case TestApplicationDoStreamStreamProcedure: testApplicationDoStreamStreamHandler.ServeHTTP(w, r) case TestApplicationDoUnaryUnaryErrorProcedure: testApplicationDoUnaryUnaryErrorHandler.ServeHTTP(w, r) case TestApplicationDoUnaryStreamErrorProcedure: testApplicationDoUnaryStreamErrorHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedTestApplicationHandler returns CodeUnimplemented from all methods. type UnimplementedTestApplicationHandler struct{} func (UnimplementedTestApplicationHandler) DoUnaryUnary(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoUnaryUnary is not implemented")) } func (UnimplementedTestApplicationHandler) DoUnaryStream(context.Context, *connect.Request[testapp.Message], *connect.ServerStream[testapp.Message]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoUnaryStream is not implemented")) } func (UnimplementedTestApplicationHandler) DoStreamUnary(context.Context, *connect.ClientStream[testapp.Message]) (*connect.Response[testapp.Message], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoStreamUnary is not implemented")) } func (UnimplementedTestApplicationHandler) DoStreamStream(context.Context, *connect.BidiStream[testapp.Message, testapp.Message]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoStreamStream is not implemented")) } func (UnimplementedTestApplicationHandler) DoUnaryUnaryError(context.Context, *connect.Request[testapp.Message]) (*connect.Response[testapp.Message], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoUnaryUnaryError is not implemented")) } func (UnimplementedTestApplicationHandler) DoUnaryStreamError(context.Context, *connect.Request[testapp.Message], *connect.ServerStream[testapp.Message]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("TestApplication.DoUnaryStreamError is not implemented")) } go-agent-3.42.0/v3/integrations/nrecho-v3/000077500000000000000000000000001510742411500201435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrecho-v3/LICENSE.txt000066400000000000000000000264501510742411500217750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrecho-v3/README.md000066400000000000000000000007261510742411500214270ustar00rootroot00000000000000# v3/integrations/nrecho-v3 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3) Package `nrecho` instruments applications using https://github.com/labstack/echo v3. ```go import "github.com/newrelic/go-agent/v3/integrations/nrecho-v3" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v3). go-agent-3.42.0/v3/integrations/nrecho-v3/example/000077500000000000000000000000001510742411500215765ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrecho-v3/example/main.go000066400000000000000000000020201510742411500230430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "net/http" "os" "github.com/labstack/echo" "github.com/newrelic/go-agent/v3/integrations/nrecho-v3" "github.com/newrelic/go-agent/v3/newrelic" ) func getUser(c echo.Context) error { id := c.Param("id") txn := nrecho.FromContext(c) txn.AddAttribute("userId", id) return c.String(http.StatusOK, id) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Echo App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Echo instance e := echo.New() // The New Relic Middleware should be the first middleware registered e.Use(nrecho.Middleware(app)) // Routes e.GET("/home", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) // Groups g := e.Group("/user") g.GET("/:id", getUser) // Start server e.Start(":8000") } go-agent-3.42.0/v3/integrations/nrecho-v3/go.mod000066400000000000000000000007231510742411500212530ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrecho-v3 // 1.7 is the earliest version of Go tested by v3.1.0: // https://github.com/labstack/echo/blob/v3.1.0/.travis.yml go 1.24 require ( // v3.1.0 is the earliest v3 version of Echo that works with modules due // to the github.com/rsc/letsencrypt import of v3.0.0. github.com/labstack/echo v3.1.0+incompatible github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrecho-v3/nrecho.go000066400000000000000000000102451510742411500217520ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrecho instruments applications using // https://github.com/labstack/echo v3. // // Use this package to instrument inbound requests handled by an echo.Echo // instance. // // e := echo.New() // // Add the nrecho middleware before other middlewares or routes: // e.Use(nrecho.Middleware(app)) // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrecho-v3/example/main.go package nrecho import ( "net/http" "reflect" "github.com/labstack/echo" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "echo") } // FromContext returns the Transaction from the context if present, and nil // otherwise. func FromContext(c echo.Context) *newrelic.Transaction { return newrelic.FromContext(c.Request().Context()) } func handlerPointer(handler echo.HandlerFunc) uintptr { return reflect.ValueOf(handler).Pointer() } func handlerName(router interface{}) string { val := reflect.ValueOf(router) if val.Kind() == reflect.Ptr { // for echo version v3.2.2+ val = val.Elem() } else { val = reflect.ValueOf(&router).Elem().Elem() } if name := val.FieldByName("Name"); name.IsValid() { // for echo version v3.2.2+ return name.String() } else if handler := val.FieldByName("Handler"); handler.IsValid() { return handler.String() } else { return "" } } func transactionName(c echo.Context) (string, string) { ptr := handlerPointer(c.Handler()) if ptr == handlerPointer(echo.NotFoundHandler) { return "NotFoundHandler", "" } if ptr == handlerPointer(echo.MethodNotAllowedHandler) { return "MethodNotAllowedHandler", "" } return c.Request().Method + " " + c.Path(), c.Path() } // Middleware creates Echo middleware that instruments requests. // // e := echo.New() // // Add the nrecho middleware before other middlewares or routes: // e.Use(nrecho.Middleware(app)) func Middleware(app *newrelic.Application) func(echo.HandlerFunc) echo.HandlerFunc { if nil == app { return func(next echo.HandlerFunc) echo.HandlerFunc { return next } } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { rw := c.Response().Writer tName, route := transactionName(c) txn := app.StartTransaction(tName) defer txn.End() if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, route) } txn.SetWebRequestHTTP(c.Request()) c.Response().Writer = txn.SetWebResponse(rw) // Add txn to c.Request().Context() c.SetRequest(c.Request().WithContext(newrelic.NewContext(c.Request().Context(), txn))) err = next(c) // Record the response code. The response headers are not captured // in this case because they are set after this middleware returns. // Designed to mimic the logic in echo.DefaultHTTPErrorHandler. if nil != err && !c.Response().Committed { c.Response().Writer = rw if httperr, ok := err.(*echo.HTTPError); ok { txn.SetWebResponse(nil).WriteHeader(httperr.Code) } else { txn.SetWebResponse(nil).WriteHeader(http.StatusInternalServerError) } if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header(), txn.GetLinkingMetadata().TraceID) } } return } } } // WrapRouter extracts API endpoints from the echo instance passed to it // which is used to detect application URL mapping(api-endpoints) for provable security. // In this version of the integration, this wrapper is only necessary if you are using the New Relic security agent integration [https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsecurityagent], // but it may be enhanced to provide additional functionality in future releases. // // e := echo.New() // .... // .... // .... // // nrecho.WrapRouter(e) func WrapRouter(engine *echo.Echo) { if engine != nil && newrelic.IsSecurityAgentPresent() { router := engine.Routes() for _, r := range router { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, handlerName(r)) } } } go-agent-3.42.0/v3/integrations/nrecho-v3/nrecho_test.go000066400000000000000000000165721510742411500230220ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrecho import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestBasicRoute(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.Blob(http.StatusOK, "text/html", []byte("Hello, World!")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, UnknownCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "S", "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "200", "http.statusCode": "200", "request.method": "GET", "response.headers.contentType": "text/html", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestNilApp(t *testing.T) { e := echo.New() e.Use(Middleware(nil)) e.GET("/hello", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } } func TestTransactionContext(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { txn := FromContext(c) txn.NoticeError(errors.New("ooops")) return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestNotFoundHandler(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "NotFoundHandler", IsWeb: true, UnknownCaller: true, }) } func TestMethodNotAllowedHandler(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("POST", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "MethodNotAllowedHandler", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestReturnsHTTPError(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return echo.NewHTTPError(http.StatusTeapot, "I'm a teapot!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "418", "http.statusCode": "418", "request.method": "GET", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestReturnsError(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return errors.New("ooooooooops") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "500", "http.statusCode": "500", "request.method": "GET", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestResponseCode(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.Blob(http.StatusTeapot, "text/html", []byte("Hello, World!")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "418", "http.statusCode": "418", "request.method": "GET", "response.headers.contentType": "text/html", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } go-agent-3.42.0/v3/integrations/nrecho-v4/000077500000000000000000000000001510742411500201445ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrecho-v4/LICENSE.txt000066400000000000000000000264501510742411500217760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrecho-v4/README.md000066400000000000000000000007271510742411500214310ustar00rootroot00000000000000# v3/integrations/nrecho-v4 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4) Package `nrecho` instruments applications using https://github.com/labstack/echo v4. ```go import "github.com/newrelic/go-agent/v3/integrations/nrecho-v4" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrecho-v4). go-agent-3.42.0/v3/integrations/nrecho-v4/example/000077500000000000000000000000001510742411500215775ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrecho-v4/example/main.go000066400000000000000000000020231510742411500230470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "net/http" "os" "github.com/labstack/echo/v4" "github.com/newrelic/go-agent/v3/integrations/nrecho-v4" "github.com/newrelic/go-agent/v3/newrelic" ) func getUser(c echo.Context) error { id := c.Param("id") txn := nrecho.FromContext(c) txn.AddAttribute("userId", id) return c.String(http.StatusOK, id) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Echo App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Echo instance e := echo.New() // The New Relic Middleware should be the first middleware registered e.Use(nrecho.Middleware(app)) // Routes e.GET("/home", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) // Groups g := e.Group("/user") g.GET("/:id", getUser) // Start server e.Start(":8000") } go-agent-3.42.0/v3/integrations/nrecho-v4/go.mod000066400000000000000000000004761510742411500212610ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrecho-v4 // As of Jun 2022, the echo go.mod file uses 1.17: // https://github.com/labstack/echo/blob/master/go.mod go 1.24 require ( github.com/labstack/echo/v4 v4.9.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrecho-v4/nrecho.go000066400000000000000000000110301510742411500217440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrecho instruments applications using // https://github.com/labstack/echo v4. // // Use this package to instrument inbound requests handled by an echo.Echo // instance. // // e := echo.New() // // Add the nrecho middleware before other middlewares or routes: // e.Use(nrecho.Middleware(app)) // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrecho-v4/example/main.go package nrecho import ( "net/http" "reflect" "github.com/labstack/echo/v4" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "echo") } // FromContext returns the Transaction from the context if present, and nil // otherwise. func FromContext(c echo.Context) *newrelic.Transaction { return newrelic.FromContext(c.Request().Context()) } func handlerPointer(handler echo.HandlerFunc) uintptr { return reflect.ValueOf(handler).Pointer() } func transactionName(c echo.Context) (string, string) { ptr := handlerPointer(c.Handler()) if ptr == handlerPointer(echo.NotFoundHandler) { return "NotFoundHandler", "" } if ptr == handlerPointer(echo.MethodNotAllowedHandler) { return "MethodNotAllowedHandler", "" } return c.Request().Method + " " + c.Path(), c.Path() } // Skipper defines a function to skip middleware. Returning true skips processing // the middleware. type Skipper func(c echo.Context) bool // Config defines the config for the middleware. type Config struct { // App contains newrelic application. App *newrelic.Application // Skipper defines a function to skip middleware. Skipper Skipper } type ConfigOption func(*Config) func WithSkipper(skipper Skipper) ConfigOption { return func(cfg *Config) { cfg.Skipper = skipper } } // Middleware creates Echo middleware with provided config that // instruments requests. // // e := echo.New() // // Add the nrecho middleware before other middlewares or routes: // e.Use(nrecho.MiddlewareWithConfig(nrecho.Config{App: app})) func Middleware(app *newrelic.Application, opts ...ConfigOption) func(echo.HandlerFunc) echo.HandlerFunc { if app == nil { return func(next echo.HandlerFunc) echo.HandlerFunc { return next } } config := Config{ App: app, } for _, opt := range opts { opt(&config) } if config.Skipper == nil { // set default skipper config.Skipper = func(echo.Context) bool { return false } } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { if config.Skipper(c) { return next(c) } rw := c.Response().Writer tname, path := transactionName(c) txn := config.App.StartTransaction(tname) defer txn.End() if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, path) } txn.SetWebRequestHTTP(c.Request()) c.Response().Writer = txn.SetWebResponse(rw) // Add txn to c.Request().Context() c.SetRequest(c.Request().WithContext(newrelic.NewContext(c.Request().Context(), txn))) err = next(c) // Record the response code. The response headers are not captured // in this case because they are set after this middleware returns. // Designed to mimic the logic in echo.DefaultHTTPErrorHandler. if nil != err && !c.Response().Committed { c.Response().Writer = rw if httperr, ok := err.(*echo.HTTPError); ok { txn.SetWebResponse(nil).WriteHeader(httperr.Code) } else { txn.SetWebResponse(nil).WriteHeader(http.StatusInternalServerError) } if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header(), txn.GetLinkingMetadata().TraceID) } } return } } } // WrapRouter extracts API endpoints from the echo instance passed to it // which is used to detect application URL mapping(api-endpoints) for provable security. // In this version of the integration, this wrapper is only necessary if you are using the New Relic security agent integration [https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsecurityagent], // but it may be enhanced to provide additional functionality in future releases. // // e := echo.New() // .... // .... // .... // // nrecho.WrapRouter(e) func WrapRouter(engine *echo.Echo) { if engine != nil && newrelic.IsSecurityAgentPresent() { router := engine.Routes() for _, r := range router { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, r.Name) } } } go-agent-3.42.0/v3/integrations/nrecho-v4/nrecho_test.go000066400000000000000000000223661510742411500230210ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrecho import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestBasicRoute(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.Blob(http.StatusOK, "text/html", []byte("Hello, World!")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, UnknownCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "S", "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "200", "http.statusCode": "200", "request.method": "GET", "response.headers.contentType": "text/html", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestSkipper(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() skipper := func(c echo.Context) bool { return c.Path() == "/health" } e.Use(Middleware(app.Application, WithSkipper(skipper))) e.GET("/hello", func(c echo.Context) error { return c.Blob(http.StatusOK, "text/html", []byte("Hello, World!")) }) e.GET("/health", func(c echo.Context) error { return c.NoContent(http.StatusNoContent) }) // call /hello endpoint (should be traced) helloResp := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(helloResp, req) if respBody := helloResp.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } // call /health endpoint (should NOT be traced) healthResp := httptest.NewRecorder() req, err = http.NewRequest("GET", "/health", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(healthResp, req) if healthResp.Code != http.StatusNoContent { t.Errorf("wrong response status code; expected: %d; got: %d", http.StatusNoContent, healthResp.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, UnknownCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "S", "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "200", "http.statusCode": "200", "request.method": "GET", "response.headers.contentType": "text/html", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestNilApp(t *testing.T) { e := echo.New() e.Use(Middleware(nil)) e.GET("/hello", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } } func TestTransactionContext(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { txn := FromContext(c) txn.NoticeError(errors.New("ooops")) return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestNotFoundHandler(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "NotFoundHandler", IsWeb: true, UnknownCaller: true, }) } func TestMethodNotAllowedHandler(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) response := httptest.NewRecorder() req, err := http.NewRequest("POST", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "MethodNotAllowedHandler", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestReturnsHTTPError(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return echo.NewHTTPError(http.StatusTeapot, "I'm a teapot!") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "418", "http.statusCode": "418", "request.method": "GET", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestReturnsError(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return errors.New("ooooooooops") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "500", "http.statusCode": "500", "request.method": "GET", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } func TestResponseCode(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) e.GET("/hello", func(c echo.Context) error { return c.Blob(http.StatusTeapot, "text/html", []byte("Hello, World!")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello?remove=me", nil) if err != nil { t.Fatal(err) } e.ServeHTTP(response, req) app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello", "nr.apdexPerfZone": "F", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": "418", "http.statusCode": "418", "request.method": "GET", "response.headers.contentType": "text/html", "request.uri": "/hello", }, UserAttributes: map[string]interface{}{}, }}) } go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/000077500000000000000000000000001510742411500220435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/LICENSE.txt000066400000000000000000000264501510742411500236750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/README.md000066400000000000000000000010201510742411500233130ustar00rootroot00000000000000# v3/integrations/nrelasticsearch-v7 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7) Package `nrelasticsearch` instruments `"github.com/elastic/go-elasticsearch/v7"`. ```go import nrelasticsearch "github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7). go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/example/000077500000000000000000000000001510742411500234765ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/example/main.go000066400000000000000000000027301510742411500247530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "encoding/json" "fmt" "os" "time" elasticsearch "github.com/elastic/go-elasticsearch/v7" nrelasticsearch "github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // Step 1: Use nrelasticsearch.NewRoundTripper to assign the // elasticsearch.Config's Transport field. cfg := elasticsearch.Config{ Transport: nrelasticsearch.NewRoundTripper(nil), } app, err := newrelic.NewApplication( newrelic.ConfigAppName("Elastic App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("elastic") // Step 2: Ensure that all calls using the elasticsearch client have a // context which includes the newrelic.Transaction. ctx := newrelic.NewContext(context.Background(), txn) es, err := elasticsearch.NewClient(cfg) if err != nil { panic(err) } res, err := es.Info(es.Info.WithContext(ctx)) if err != nil { panic(err) } if res.IsError() { panic(err) } var r map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&r); err != nil { panic(err) } fmt.Println("ELASTIC SEARCH INFO", elasticsearch.Version, r["version"].(map[string]interface{})["number"]) txn.End() app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/example_test.go000066400000000000000000000016461510742411500250730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrelasticsearch_test import ( "context" elasticsearch "github.com/elastic/go-elasticsearch/v7" nrelasticsearch "github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7" "github.com/newrelic/go-agent/v3/newrelic" ) func getTransaction() *newrelic.Transaction { return nil } func Example() { // Step 1: Use nrelasticsearch.NewRoundTripper to assign the // elasticsearch.Config's Transport field. cfg := elasticsearch.Config{ Transport: nrelasticsearch.NewRoundTripper(nil), } client, err := elasticsearch.NewClient(cfg) if err != nil { panic(err) } // Step 2: Ensure that all calls using the elasticsearch client have // a context which includes the newrelic.Transaction. txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) client.Info(client.Info.WithContext(ctx)) } go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/go.mod000066400000000000000000000005421510742411500231520ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7 // As of Jan 2020, the v7 elasticsearch go.mod uses 1.11: // https://github.com/elastic/go-elasticsearch/blob/7.x/go.mod go 1.24 require ( github.com/elastic/go-elasticsearch/v7 v7.17.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/internal/000077500000000000000000000000001510742411500236575ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/internal/http-endpoint-list-utility/000077500000000000000000000000001510742411500311265ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/internal/http-endpoint-list-utility/endpoints.txt000066400000000000000000000667571510742411500337170ustar00rootroot00000000000000 delete.json DELETE /{index}/{type}/{id} exists.json HEAD /{index}/{type}/{id} get.json GET /{index}/{type}/{id} index.json POST /{index}/{type}/{id} index.json PUT /{index}/{type}/{id} index.json POST /{index}/{type} indices.create.json PUT /{index} indices.delete.json DELETE /{index} indices.exists.json HEAD /{index} indices.get.json GET /{index} info.json GET / ping.json HEAD / _alias indices.delete_alias.json DELETE /{index}/_alias/{name} _alias indices.exists_alias.json HEAD /_alias/{name} _alias indices.exists_alias.json HEAD /{index}/_alias/{name} _alias indices.get_alias.json GET /{index}/_alias/{name} _alias indices.get_alias.json GET /_alias/{name} _alias indices.get_alias.json GET /_alias _alias indices.get_alias.json GET /{index}/_alias _alias indices.put_alias.json POST /{index}/_alias/{name} _alias indices.put_alias.json PUT /{index}/_alias/{name} _aliases indices.delete_alias.json DELETE /{index}/_aliases/{name} _aliases indices.put_alias.json POST /{index}/_aliases/{name} _aliases indices.put_alias.json PUT /{index}/_aliases/{name} _aliases indices.update_aliases.json POST /_aliases _analyze indices.analyze.json GET /{index}/_analyze _analyze indices.analyze.json GET /_analyze _analyze indices.analyze.json POST /_analyze _analyze indices.analyze.json POST /{index}/_analyze _bulk bulk.json POST /_bulk _bulk bulk.json PUT /{index}/{type}/_bulk _bulk bulk.json POST /{index}/{type}/_bulk _bulk bulk.json PUT /{index}/_bulk _bulk bulk.json POST /{index}/_bulk _bulk bulk.json PUT /_bulk _cache indices.clear_cache.json POST /{index}/_cache/clear _cache indices.clear_cache.json POST /_cache/clear _cat cat.aliases.json GET /_cat/aliases/{name} _cat cat.aliases.json GET /_cat/aliases _cat cat.allocation.json GET /_cat/allocation/{node_id} _cat cat.allocation.json GET /_cat/allocation _cat cat.count.json GET /_cat/count _cat cat.count.json GET /_cat/count/{index} _cat cat.fielddata.json GET /_cat/fielddata/{fields} _cat cat.fielddata.json GET /_cat/fielddata _cat cat.health.json GET /_cat/health _cat cat.help.json GET /_cat _cat cat.indices.json GET /_cat/indices/{index} _cat cat.indices.json GET /_cat/indices _cat cat.master.json GET /_cat/master _cat cat.nodeattrs.json GET /_cat/nodeattrs _cat cat.nodes.json GET /_cat/nodes _cat cat.pending_tasks.json GET /_cat/pending_tasks _cat cat.plugins.json GET /_cat/plugins _cat cat.recovery.json GET /_cat/recovery _cat cat.recovery.json GET /_cat/recovery/{index} _cat cat.repositories.json GET /_cat/repositories _cat cat.segments.json GET /_cat/segments _cat cat.segments.json GET /_cat/segments/{index} _cat cat.shards.json GET /_cat/shards/{index} _cat cat.shards.json GET /_cat/shards _cat cat.snapshots.json GET /_cat/snapshots/{repository} _cat cat.snapshots.json GET /_cat/snapshots _cat cat.tasks.json GET /_cat/tasks _cat cat.templates.json GET /_cat/templates _cat cat.templates.json GET /_cat/templates/{name} _cat cat.thread_pool.json GET /_cat/thread_pool _cat cat.thread_pool.json GET /_cat/thread_pool/{thread_pool_patterns} _clone indices.clone.json PUT /{index}/_clone/{target} _clone indices.clone.json POST /{index}/_clone/{target} _close indices.close.json POST /{index}/_close _cluster cluster.allocation_explain.json GET /_cluster/allocation/explain _cluster cluster.allocation_explain.json POST /_cluster/allocation/explain _cluster cluster.get_settings.json GET /_cluster/settings _cluster cluster.health.json GET /_cluster/health _cluster cluster.health.json GET /_cluster/health/{index} _cluster cluster.pending_tasks.json GET /_cluster/pending_tasks _cluster cluster.put_settings.json PUT /_cluster/settings _cluster cluster.reroute.json POST /_cluster/reroute _cluster cluster.state.json GET /_cluster/state/{metric} _cluster cluster.state.json GET /_cluster/state/{metric}/{index} _cluster cluster.state.json GET /_cluster/state _cluster cluster.stats.json GET /_cluster/stats _cluster cluster.stats.json GET /_cluster/stats/nodes/{node_id} _cluster nodes.hot_threads.json GET /_cluster/nodes/hotthreads _cluster nodes.hot_threads.json GET /_cluster/nodes/{node_id}/hotthreads _cluster nodes.hot_threads.json GET /_cluster/nodes/{node_id}/hot_threads _cluster nodes.hot_threads.json GET /_cluster/nodes/hot_threads _count count.json GET /{index}/_count _count count.json POST /{index}/_count _count count.json POST /{index}/{type}/_count _count count.json GET /{index}/{type}/_count _count count.json GET /_count _count count.json POST /_count _create create.json POST /{index}/_create/{id} _create create.json POST /{index}/{type}/{id}/_create _create create.json PUT /{index}/_create/{id} _create create.json PUT /{index}/{type}/{id}/_create _delete_by_query delete_by_query.json POST /{index}/{type}/_delete_by_query _delete_by_query delete_by_query.json POST /{index}/_delete_by_query _delete_by_query delete_by_query_rethrottle.json POST /_delete_by_query/{task_id}/_rethrottle _doc delete.json DELETE /{index}/_doc/{id} _doc exists.json HEAD /{index}/_doc/{id} _doc get.json GET /{index}/_doc/{id} _doc index.json POST /{index}/_doc/{id} _doc index.json POST /{index}/_doc _doc index.json PUT /{index}/_doc/{id} _explain explain.json POST /{index}/_explain/{id} _explain explain.json GET /{index}/{type}/{id}/_explain _explain explain.json POST /{index}/{type}/{id}/_explain _explain explain.json GET /{index}/_explain/{id} _field_caps field_caps.json POST /_field_caps _field_caps field_caps.json GET /_field_caps _field_caps field_caps.json GET /{index}/_field_caps _field_caps field_caps.json POST /{index}/_field_caps _flush indices.flush.json GET /{index}/_flush _flush indices.flush.json POST /_flush _flush indices.flush.json GET /_flush _flush indices.flush.json POST /{index}/_flush _flush indices.flush_synced.json POST /{index}/_flush/synced _flush indices.flush_synced.json GET /_flush/synced _flush indices.flush_synced.json POST /_flush/synced _flush indices.flush_synced.json GET /{index}/_flush/synced _forcemerge indices.forcemerge.json POST /_forcemerge _forcemerge indices.forcemerge.json POST /{index}/_forcemerge _ingest ingest.delete_pipeline.json DELETE /_ingest/pipeline/{id} _ingest ingest.get_pipeline.json GET /_ingest/pipeline _ingest ingest.get_pipeline.json GET /_ingest/pipeline/{id} _ingest ingest.processor_grok.json GET /_ingest/processor/grok _ingest ingest.put_pipeline.json PUT /_ingest/pipeline/{id} _ingest ingest.simulate.json GET /_ingest/pipeline/_simulate _ingest ingest.simulate.json POST /_ingest/pipeline/{id}/_simulate _ingest ingest.simulate.json POST /_ingest/pipeline/_simulate _ingest ingest.simulate.json GET /_ingest/pipeline/{id}/_simulate _mapping indices.exists_type.json HEAD /{index}/_mapping/{type} _mapping indices.get_field_mapping.json GET /{index}/_mapping/field/{fields} _mapping indices.get_field_mapping.json GET /_mapping/field/{fields} _mapping indices.get_field_mapping.json GET /{index}/_mapping/{type}/field/{fields} _mapping indices.get_field_mapping.json GET /_mapping/{type}/field/{fields} _mapping indices.get_mapping.json GET /{index}/_mapping/{type} _mapping indices.get_mapping.json GET /_mapping _mapping indices.get_mapping.json GET /_mapping/{type} _mapping indices.get_mapping.json GET /{index}/_mapping _mapping indices.put_mapping.json PUT /{index}/_mapping/{type} _mapping indices.put_mapping.json POST /{index}/_mapping/{type} _mapping indices.put_mapping.json POST /_mapping/{type} _mapping indices.put_mapping.json PUT /_mapping/{type} _mapping indices.put_mapping.json PUT {index}/_mapping _mapping indices.put_mapping.json POST {index}/_mapping _mapping indices.put_mapping.json PUT /{index}/{type}/_mapping _mapping indices.put_mapping.json POST /{index}/{type}/_mapping _mappings indices.put_mapping.json PUT {index}/_mappings _mappings indices.put_mapping.json POST {index}/_mappings _mappings indices.put_mapping.json PUT /{index}/{type}/_mappings _mappings indices.put_mapping.json POST /{index}/_mappings/{type} _mappings indices.put_mapping.json PUT /{index}/_mappings/{type} _mappings indices.put_mapping.json POST /{index}/{type}/_mappings _mappings indices.put_mapping.json PUT /_mappings/{type} _mappings indices.put_mapping.json POST /_mappings/{type} _mget mget.json POST /{index}/{type}/_mget _mget mget.json POST /{index}/_mget _mget mget.json POST /_mget _mget mget.json GET /_mget _mget mget.json GET /{index}/{type}/_mget _mget mget.json GET /{index}/_mget _msearch msearch.json GET /_msearch _msearch msearch.json POST /{index}/{type}/_msearch _msearch msearch.json GET /{index}/{type}/_msearch _msearch msearch.json POST /{index}/_msearch _msearch msearch.json GET /{index}/_msearch _msearch msearch.json POST /_msearch _msearch msearch_template.json GET /{index}/{type}/_msearch/template _msearch msearch_template.json POST /_msearch/template _msearch msearch_template.json GET /{index}/_msearch/template _msearch msearch_template.json POST /{index}/{type}/_msearch/template _msearch msearch_template.json GET /_msearch/template _msearch msearch_template.json POST /{index}/_msearch/template _mtermvectors mtermvectors.json POST /{index}/_mtermvectors _mtermvectors mtermvectors.json GET /_mtermvectors _mtermvectors mtermvectors.json POST /_mtermvectors _mtermvectors mtermvectors.json GET /{index}/{type}/_mtermvectors _mtermvectors mtermvectors.json POST /{index}/{type}/_mtermvectors _mtermvectors mtermvectors.json GET /{index}/_mtermvectors _nodes nodes.hot_threads.json GET /_nodes/{node_id}/hotthreads _nodes nodes.hot_threads.json GET /_nodes/hotthreads _nodes nodes.hot_threads.json GET /_nodes/hot_threads _nodes nodes.hot_threads.json GET /_nodes/{node_id}/hot_threads _nodes nodes.info.json GET /_nodes _nodes nodes.info.json GET /_nodes/{node_id} _nodes nodes.info.json GET /_nodes/{metric} _nodes nodes.info.json GET /_nodes/{node_id}/{metric} _nodes nodes.reload_secure_settings.json POST /_nodes/{node_id}/reload_secure_settings _nodes nodes.reload_secure_settings.json POST /_nodes/reload_secure_settings _nodes nodes.stats.json GET /_nodes/{node_id}/stats/{metric}/{index_metric} _nodes nodes.stats.json GET /_nodes/stats/{metric}/{index_metric} _nodes nodes.stats.json GET /_nodes/stats _nodes nodes.stats.json GET /_nodes/{node_id}/stats _nodes nodes.stats.json GET /_nodes/stats/{metric} _nodes nodes.stats.json GET /_nodes/{node_id}/stats/{metric} _nodes nodes.usage.json GET /_nodes/usage/{metric} _nodes nodes.usage.json GET /_nodes/{node_id}/usage/{metric} _nodes nodes.usage.json GET /_nodes/{node_id}/usage _nodes nodes.usage.json GET /_nodes/usage _open indices.open.json POST /{index}/_open _rank_eval rank_eval.json POST /_rank_eval _rank_eval rank_eval.json GET /{index}/_rank_eval _rank_eval rank_eval.json POST /{index}/_rank_eval _rank_eval rank_eval.json GET /_rank_eval _recovery indices.recovery.json GET /_recovery _recovery indices.recovery.json GET /{index}/_recovery _refresh indices.refresh.json POST /_refresh _refresh indices.refresh.json GET /{index}/_refresh _refresh indices.refresh.json POST /{index}/_refresh _refresh indices.refresh.json GET /_refresh _reindex reindex.json POST /_reindex _reindex reindex_rethrottle.json POST /_reindex/{task_id}/_rethrottle _remote cluster.remote_info.json GET /_remote/info _render render_search_template.json GET /_render/template/{id} _render render_search_template.json POST /_render/template/{id} _render render_search_template.json GET /_render/template _render render_search_template.json POST /_render/template _rollover indices.rollover.json POST /{alias}/_rollover/{new_index} _rollover indices.rollover.json POST /{alias}/_rollover _scripts delete_script.json DELETE /_scripts/{id} _scripts get_script.json GET /_scripts/{id} _scripts put_script.json POST /_scripts/{id}/{context} _scripts put_script.json PUT /_scripts/{id}/{context} _scripts put_script.json POST /_scripts/{id} _scripts put_script.json PUT /_scripts/{id} _scripts scripts_painless_execute.json GET /_scripts/painless/_execute _scripts scripts_painless_execute.json POST /_scripts/painless/_execute _search clear_scroll.json DELETE /_search/scroll _search clear_scroll.json DELETE /_search/scroll/{scroll_id} _search scroll.json POST /_search/scroll _search scroll.json GET /_search/scroll/{scroll_id} _search scroll.json GET /_search/scroll _search scroll.json POST /_search/scroll/{scroll_id} _search search.json POST /_search _search search.json GET /_search _search search.json POST /{index}/_search _search search.json GET /{index}/{type}/_search _search search.json POST /{index}/{type}/_search _search search.json GET /{index}/_search _search search_template.json GET /_search/template _search search_template.json POST /_search/template _search search_template.json GET /{index}/_search/template _search search_template.json GET /{index}/{type}/_search/template _search search_template.json POST /{index}/{type}/_search/template _search search_template.json POST /{index}/_search/template _search_shards search_shards.json GET /{index}/_search_shards _search_shards search_shards.json GET /_search_shards _search_shards search_shards.json POST /{index}/_search_shards _search_shards search_shards.json POST /_search_shards _segments indices.segments.json GET /_segments _segments indices.segments.json GET /{index}/_segments _settings indices.get_settings.json GET /_settings _settings indices.get_settings.json GET /{index}/_settings/{name} _settings indices.get_settings.json GET /_settings/{name} _settings indices.get_settings.json GET /{index}/_settings _settings indices.put_settings.json PUT /_settings _settings indices.put_settings.json PUT /{index}/_settings _shard_stores indices.shard_stores.json GET /{index}/_shard_stores _shard_stores indices.shard_stores.json GET /_shard_stores _shrink indices.shrink.json PUT /{index}/_shrink/{target} _shrink indices.shrink.json POST /{index}/_shrink/{target} _snapshot snapshot.cleanup_repository.json POST /_snapshot/{repository}/_cleanup _snapshot snapshot.create.json PUT /_snapshot/{repository}/{snapshot} _snapshot snapshot.create.json POST /_snapshot/{repository}/{snapshot} _snapshot snapshot.create_repository.json PUT /_snapshot/{repository} _snapshot snapshot.create_repository.json POST /_snapshot/{repository} _snapshot snapshot.delete.json DELETE /_snapshot/{repository}/{snapshot} _snapshot snapshot.delete_repository.json DELETE /_snapshot/{repository} _snapshot snapshot.get.json GET /_snapshot/{repository}/{snapshot} _snapshot snapshot.get_repository.json GET /_snapshot/{repository} _snapshot snapshot.get_repository.json GET /_snapshot _snapshot snapshot.restore.json POST /_snapshot/{repository}/{snapshot}/_restore _snapshot snapshot.status.json GET /_snapshot/{repository}/{snapshot}/_status _snapshot snapshot.status.json GET /_snapshot/{repository}/_status _snapshot snapshot.status.json GET /_snapshot/_status _snapshot snapshot.verify_repository.json POST /_snapshot/{repository}/_verify _source exists_source.json HEAD /{index}/_source/{id} _source exists_source.json HEAD /{index}/{type}/{id}/_source _source get_source.json GET /{index}/_source/{id} _source get_source.json GET /{index}/{type}/{id}/_source _split indices.split.json POST /{index}/_split/{target} _split indices.split.json PUT /{index}/_split/{target} _stats indices.stats.json GET /{index}/_stats _stats indices.stats.json GET /_stats _stats indices.stats.json GET /_stats/{metric} _stats indices.stats.json GET /{index}/_stats/{metric} _tasks tasks.cancel.json POST /_tasks/{task_id}/_cancel _tasks tasks.cancel.json POST /_tasks/_cancel _tasks tasks.get.json GET /_tasks/{task_id} _tasks tasks.list.json GET /_tasks _template indices.delete_template.json DELETE /_template/{name} _template indices.exists_template.json HEAD /_template/{name} _template indices.get_template.json GET /_template _template indices.get_template.json GET /_template/{name} _template indices.put_template.json PUT /_template/{name} _template indices.put_template.json POST /_template/{name} _termvectors termvectors.json GET /{index}/_termvectors/{id} _termvectors termvectors.json POST /{index}/_termvectors _termvectors termvectors.json POST /{index}/{type}/{id}/_termvectors _termvectors termvectors.json GET /{index}/{type}/_termvectors _termvectors termvectors.json GET /{index}/{type}/{id}/_termvectors _termvectors termvectors.json POST /{index}/{type}/_termvectors _termvectors termvectors.json GET /{index}/_termvectors _termvectors termvectors.json POST /{index}/_termvectors/{id} _update update.json POST /{index}/_update/{id} _update update.json POST /{index}/{type}/{id}/_update _update_by_query update_by_query.json POST /{index}/_update_by_query _update_by_query update_by_query.json POST /{index}/{type}/_update_by_query _update_by_query update_by_query_rethrottle.json POST /_update_by_query/{task_id}/_rethrottle _upgrade indices.get_upgrade.json GET /{index}/_upgrade _upgrade indices.get_upgrade.json GET /_upgrade _upgrade indices.upgrade.json POST /{index}/_upgrade _upgrade indices.upgrade.json POST /_upgrade _validate indices.validate_query.json GET /_validate/query _validate indices.validate_query.json POST /{index}/{type}/_validate/query _validate indices.validate_query.json POST /_validate/query _validate indices.validate_query.json GET /{index}/{type}/_validate/query _validate indices.validate_query.json POST /{index}/_validate/query _validate indices.validate_query.json GET /{index}/_validate/query go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/internal/http-endpoint-list-utility/main.go000066400000000000000000000046231510742411500324060ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main // This is a utility designed to create a list of all Elasticsearch http // endpoints. The output of this script is checked in as 'endpoints.txt'. import ( "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "sort" "strings" ) type endpoint struct { firstUnderscoreSegment string filename string method string path string } func main() { if len(os.Args) < 2 { panic("provide path to github.com/elastic/elasticsearch/tree/7.5/rest-api-spec/src/main/resources/rest-api-spec/api/") } var files []string err := filepath.Walk(os.Args[1], func(path string, info os.FileInfo, err error) error { files = append(files, path) return nil }) if err != nil { panic(err) } var endpoints []endpoint for _, path := range files { _, filename := filepath.Split(path) // Check the "_" prefix to avoid parsing _common. if filename == "" || strings.HasPrefix(filename, "_") { continue } contents, err := ioutil.ReadFile(path) if nil != err { panic(err) } var fields map[string]struct { Stability string `json:"stability"` URL struct { Paths []struct { Path string `json:"path"` Methods []string `json:"methods"` } `json:"paths"` } `json:"url"` } err = json.Unmarshal(contents, &fields) if nil != err { panic(err) } for _, v := range fields { for _, p := range v.URL.Paths { path := p.Path segments := strings.Split(strings.TrimPrefix(path, "/"), "/") var firstUnderscoreSegment string for _, s := range segments { if strings.HasPrefix(s, "_") { firstUnderscoreSegment = s break } } for _, method := range p.Methods { endpoints = append(endpoints, endpoint{ firstUnderscoreSegment: firstUnderscoreSegment, filename: filename, method: method, path: path, }) } } } } sort.Slice(endpoints, func(i int, j int) bool { iseg := endpoints[i].firstUnderscoreSegment jseg := endpoints[j].firstUnderscoreSegment if iseg == jseg { return endpoints[i].filename < endpoints[j].filename } return iseg < jseg }) for _, e := range endpoints { fmt.Printf("%-18v %-35v %-10v %v\n", e.firstUnderscoreSegment, e.filename, e.method, e.path) } } go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/nrelastic.go000066400000000000000000000105211510742411500243550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrelasticsearch instruments https://github.com/elastic/go-elasticsearch. // // Use this package to instrument your elasticsearch v7 calls without having to // manually create DatastoreSegments. package nrelasticsearch import ( "net/http" "strings" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "datastore", "elasticsearch") } func parseRequest(r *http.Request) (segment newrelic.DatastoreSegment) { segment.StartTime = newrelic.FromContext(r.Context()).StartSegmentNow() segment.Product = newrelic.DatastoreElasticsearch path := strings.TrimPrefix(r.URL.Path, "/") method := r.Method if "" == path { switch method { case "GET": segment.Operation = "info" case "HEAD": segment.Operation = "ping" } return } segments := strings.Split(path, "/") for idx, s := range segments { switch s { case "_alias", "_aliases", "_analyze", "_bulk", "_cache", "_cat", "_clone", "_close", "_cluster", "_count", "_create", "_delete_by_query", "_explain", "_field_caps", "_flush", "_forcemerge", "_ingest", "_mapping", "_mappings", "_mget", "_msearch", "_mtermvectors", "_nodes", "_open", "_rank_eval", "_recovery", "_refresh", "_reindex", "_remote", "_render", "_rollover", "_scripts", "_search_shards", "_segments", "_settings", "_shard_stores", "_shrink", "_snapshot", "_source", "_split", "_stats", "_tasks", "_template", "_termvectors", "_update", "_update_by_query", "_upgrade", "_validate": segment.Operation = strings.TrimPrefix(s, "_") if idx > 0 { segment.Collection = segments[0] } return case "_doc": switch method { case "DELETE": segment.Operation = "delete" case "HEAD": segment.Operation = "exists" case "GET": segment.Operation = "get" case "PUT": segment.Operation = "update" case "POST": segment.Operation = "create" } if idx > 0 { segment.Collection = segments[0] } return case "_search": // clear_scroll.json DELETE /_search/scroll // clear_scroll.json DELETE /_search/scroll/{scroll_id} // scroll.json GET /_search/scroll // scroll.json GET /_search/scroll/{scroll_id} // scroll.json POST /_search/scroll // scroll.json POST /_search/scroll/{scroll_id} // search.json GET /_search // search.json GET /{index}/_search // search.json GET /{index}/{type}/_search // search.json POST /_search // search.json POST /{index}/_search // search.json POST /{index}/{type}/_search // search_template.json GET /_search/template // search_template.json GET /{index}/_search/template // search_template.json GET /{index}/{type}/_search/template // search_template.json POST /_search/template // search_template.json POST /{index}/_search/template // search_template.json POST /{index}/{type}/_search/template if method == "DELETE" { segment.Operation = "clear_scroll" return } if idx == len(segments)-1 { segment.Operation = "search" if idx > 0 { segment.Collection = segments[0] } return } next := segments[idx+1] if next == "scroll" { segment.Operation = "scroll" return } if next == "template" { segment.Operation = "search_template" if idx > 0 { segment.Collection = segments[0] } return } return } } return } type roundtripper struct{ original http.RoundTripper } func (t roundtripper) RoundTrip(r *http.Request) (*http.Response, error) { segment := parseRequest(r) defer segment.End() return t.original.RoundTrip(r) } // NewRoundTripper creates a new http.RoundTripper to instrument elasticsearch // calls. If an http.RoundTripper parameter is not provided, then the returned // http.RoundTripper will delegate to http.DefaultTransport. func NewRoundTripper(original http.RoundTripper) http.RoundTripper { if nil == original { original = http.DefaultTransport } return roundtripper{original: original} } go-agent-3.42.0/v3/integrations/nrelasticsearch-v7/nrelastic_test.go000066400000000000000000000762171510742411500254320ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrelasticsearch import ( "context" "errors" "net/http" "net/url" "strings" "testing" elasticsearch "github.com/elastic/go-elasticsearch/v7" "github.com/elastic/go-elasticsearch/v7/esapi" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestParseRequest(t *testing.T) { testcases := []struct { // Input Method string Path string // Expect Collection string Operation string }{ // These index operations are not worth worrying about. They // are only going to be used in db setup. {Method: "DELETE", Path: "/{index}/{type}/{id}", Collection: "", Operation: ""}, {Method: "HEAD", Path: "/{index}/{type}/{id}", Collection: "", Operation: ""}, {Method: "GET", Path: "/{index}/{type}/{id}", Collection: "", Operation: ""}, {Method: "POST", Path: "/{index}/{type}", Collection: "", Operation: ""}, {Method: "POST", Path: "/{index}/{type}/{id}", Collection: "", Operation: ""}, {Method: "PUT", Path: "/{index}/{type}/{id}", Collection: "", Operation: ""}, {Method: "PUT", Path: "/{index}", Collection: "", Operation: ""}, {Method: "DELETE", Path: "/{index}", Collection: "", Operation: ""}, {Method: "HEAD", Path: "/{index}", Collection: "", Operation: ""}, {Method: "GET", Path: "/{index}", Collection: "", Operation: ""}, {Method: "GET", Path: "/", Collection: "", Operation: "info"}, {Method: "HEAD", Path: "/", Collection: "", Operation: "ping"}, {Method: "DELETE", Path: "/{index}/_alias/{name}", Collection: "{index}", Operation: "alias"}, {Method: "HEAD", Path: "/_alias/{name}", Operation: "alias"}, {Method: "HEAD", Path: "/{index}/_alias/{name}", Collection: "{index}", Operation: "alias"}, {Method: "GET", Path: "/_alias", Operation: "alias"}, {Method: "GET", Path: "/_alias/{name}", Operation: "alias"}, {Method: "GET", Path: "/{index}/_alias", Collection: "{index}", Operation: "alias"}, {Method: "GET", Path: "/{index}/_alias/{name}", Collection: "{index}", Operation: "alias"}, {Method: "POST", Path: "/{index}/_alias/{name}", Collection: "{index}", Operation: "alias"}, {Method: "PUT", Path: "/{index}/_alias/{name}", Collection: "{index}", Operation: "alias"}, {Method: "DELETE", Path: "/{index}/_aliases/{name}", Collection: "{index}", Operation: "aliases"}, {Method: "POST", Path: "/{index}/_aliases/{name}", Collection: "{index}", Operation: "aliases"}, {Method: "PUT", Path: "/{index}/_aliases/{name}", Collection: "{index}", Operation: "aliases"}, {Method: "POST", Path: "/_aliases", Operation: "aliases"}, {Method: "GET", Path: "/_analyze", Operation: "analyze"}, {Method: "GET", Path: "/{index}/_analyze", Collection: "{index}", Operation: "analyze"}, {Method: "POST", Path: "/_analyze", Operation: "analyze"}, {Method: "POST", Path: "/{index}/_analyze", Collection: "{index}", Operation: "analyze"}, {Method: "POST", Path: "/_bulk", Operation: "bulk"}, {Method: "POST", Path: "/{index}/_bulk", Collection: "{index}", Operation: "bulk"}, {Method: "POST", Path: "/{index}/{type}/_bulk", Collection: "{index}", Operation: "bulk"}, {Method: "PUT", Path: "/_bulk", Operation: "bulk"}, {Method: "PUT", Path: "/{index}/_bulk", Collection: "{index}", Operation: "bulk"}, {Method: "PUT", Path: "/{index}/{type}/_bulk", Collection: "{index}", Operation: "bulk"}, {Method: "POST", Path: "/_cache/clear", Operation: "cache"}, {Method: "POST", Path: "/{index}/_cache/clear", Collection: "{index}", Operation: "cache"}, {Method: "GET", Path: "/_cat/aliases", Operation: "cat"}, {Method: "GET", Path: "/_cat/aliases/{name}", Operation: "cat"}, {Method: "GET", Path: "/_cat/allocation", Operation: "cat"}, {Method: "GET", Path: "/_cat/allocation/{node_id}", Operation: "cat"}, {Method: "GET", Path: "/_cat/count", Operation: "cat"}, {Method: "GET", Path: "/_cat/count/{index}", Collection: "", Operation: "cat"}, {Method: "GET", Path: "/_cat/fielddata", Operation: "cat"}, {Method: "GET", Path: "/_cat/fielddata/{fields}", Operation: "cat"}, {Method: "GET", Path: "/_cat/health", Operation: "cat"}, {Method: "GET", Path: "/_cat", Operation: "cat"}, {Method: "GET", Path: "/_cat/indices", Operation: "cat"}, {Method: "GET", Path: "/_cat/indices/{index}", Collection: "", Operation: "cat"}, {Method: "GET", Path: "/_cat/master", Operation: "cat"}, {Method: "GET", Path: "/_cat/nodeattrs", Operation: "cat"}, {Method: "GET", Path: "/_cat/nodes", Operation: "cat"}, {Method: "GET", Path: "/_cat/pending_tasks", Operation: "cat"}, {Method: "GET", Path: "/_cat/plugins", Operation: "cat"}, {Method: "GET", Path: "/_cat/recovery", Operation: "cat"}, {Method: "GET", Path: "/_cat/recovery/{index}", Collection: "", Operation: "cat"}, {Method: "GET", Path: "/_cat/repositories", Operation: "cat"}, {Method: "GET", Path: "/_cat/segments", Operation: "cat"}, {Method: "GET", Path: "/_cat/segments/{index}", Collection: "", Operation: "cat"}, {Method: "GET", Path: "/_cat/shards", Operation: "cat"}, {Method: "GET", Path: "/_cat/shards/{index}", Collection: "", Operation: "cat"}, {Method: "GET", Path: "/_cat/snapshots", Operation: "cat"}, {Method: "GET", Path: "/_cat/snapshots/{repository}", Operation: "cat"}, {Method: "GET", Path: "/_cat/tasks", Operation: "cat"}, {Method: "GET", Path: "/_cat/templates", Operation: "cat"}, {Method: "GET", Path: "/_cat/templates/{name}", Operation: "cat"}, {Method: "GET", Path: "/_cat/thread_pool", Operation: "cat"}, {Method: "GET", Path: "/_cat/thread_pool/{thread_pool_patterns}", Operation: "cat"}, {Method: "POST", Path: "/{index}/_clone/{target}", Collection: "{index}", Operation: "clone"}, {Method: "PUT", Path: "/{index}/_clone/{target}", Collection: "{index}", Operation: "clone"}, {Method: "POST", Path: "/{index}/_close", Collection: "{index}", Operation: "close"}, {Method: "GET", Path: "/_cluster/allocation/explain", Operation: "cluster"}, {Method: "POST", Path: "/_cluster/allocation/explain", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/settings", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/health", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/health/{index}", Collection: "", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/pending_tasks", Operation: "cluster"}, {Method: "PUT", Path: "/_cluster/settings", Operation: "cluster"}, {Method: "POST", Path: "/_cluster/reroute", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/state", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/state/{metric}", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/state/{metric}/{index}", Collection: "", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/stats", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/stats/nodes/{node_id}", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/nodes/hot_threads", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/nodes/hotthreads", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/nodes/{node_id}/hot_threads", Operation: "cluster"}, {Method: "GET", Path: "/_cluster/nodes/{node_id}/hotthreads", Operation: "cluster"}, {Method: "GET", Path: "/_count", Operation: "count"}, {Method: "GET", Path: "/{index}/_count", Collection: "{index}", Operation: "count"}, {Method: "GET", Path: "/{index}/{type}/_count", Collection: "{index}", Operation: "count"}, {Method: "POST", Path: "/_count", Operation: "count"}, {Method: "POST", Path: "/{index}/_count", Collection: "{index}", Operation: "count"}, {Method: "POST", Path: "/{index}/{type}/_count", Collection: "{index}", Operation: "count"}, {Method: "POST", Path: "/{index}/_create/{id}", Collection: "{index}", Operation: "create"}, {Method: "POST", Path: "/{index}/{type}/{id}/_create", Collection: "{index}", Operation: "create"}, {Method: "PUT", Path: "/{index}/_create/{id}", Collection: "{index}", Operation: "create"}, {Method: "PUT", Path: "/{index}/{type}/{id}/_create", Collection: "{index}", Operation: "create"}, {Method: "POST", Path: "/{index}/_delete_by_query", Collection: "{index}", Operation: "delete_by_query"}, {Method: "POST", Path: "/{index}/{type}/_delete_by_query", Collection: "{index}", Operation: "delete_by_query"}, {Method: "POST", Path: "/_delete_by_query/{task_id}/_rethrottle", Operation: "delete_by_query"}, {Method: "DELETE", Path: "/{index}/_doc/{id}", Collection: "{index}", Operation: "delete"}, {Method: "HEAD", Path: "/{index}/_doc/{id}", Collection: "{index}", Operation: "exists"}, {Method: "GET", Path: "/{index}/_doc/{id}", Collection: "{index}", Operation: "get"}, {Method: "POST", Path: "/{index}/_doc", Collection: "{index}", Operation: "create"}, {Method: "POST", Path: "/{index}/_doc/{id}", Collection: "{index}", Operation: "create"}, {Method: "PUT", Path: "/{index}/_doc/{id}", Collection: "{index}", Operation: "update"}, {Method: "GET", Path: "/{index}/_explain/{id}", Collection: "{index}", Operation: "explain"}, {Method: "GET", Path: "/{index}/{type}/{id}/_explain", Collection: "{index}", Operation: "explain"}, {Method: "POST", Path: "/{index}/_explain/{id}", Collection: "{index}", Operation: "explain"}, {Method: "POST", Path: "/{index}/{type}/{id}/_explain", Collection: "{index}", Operation: "explain"}, {Method: "GET", Path: "/_field_caps", Operation: "field_caps"}, {Method: "GET", Path: "/{index}/_field_caps", Collection: "{index}", Operation: "field_caps"}, {Method: "POST", Path: "/_field_caps", Operation: "field_caps"}, {Method: "POST", Path: "/{index}/_field_caps", Collection: "{index}", Operation: "field_caps"}, {Method: "GET", Path: "/_flush", Operation: "flush"}, {Method: "GET", Path: "/{index}/_flush", Collection: "{index}", Operation: "flush"}, {Method: "POST", Path: "/_flush", Operation: "flush"}, {Method: "POST", Path: "/{index}/_flush", Collection: "{index}", Operation: "flush"}, {Method: "GET", Path: "/_flush/synced", Operation: "flush"}, {Method: "GET", Path: "/{index}/_flush/synced", Collection: "{index}", Operation: "flush"}, {Method: "POST", Path: "/_flush/synced", Operation: "flush"}, {Method: "POST", Path: "/{index}/_flush/synced", Collection: "{index}", Operation: "flush"}, {Method: "POST", Path: "/_forcemerge", Operation: "forcemerge"}, {Method: "POST", Path: "/{index}/_forcemerge", Collection: "{index}", Operation: "forcemerge"}, {Method: "DELETE", Path: "/_ingest/pipeline/{id}", Operation: "ingest"}, {Method: "GET", Path: "/_ingest/pipeline", Operation: "ingest"}, {Method: "GET", Path: "/_ingest/pipeline/{id}", Operation: "ingest"}, {Method: "GET", Path: "/_ingest/processor/grok", Operation: "ingest"}, {Method: "PUT", Path: "/_ingest/pipeline/{id}", Operation: "ingest"}, {Method: "GET", Path: "/_ingest/pipeline/_simulate", Operation: "ingest"}, {Method: "GET", Path: "/_ingest/pipeline/{id}/_simulate", Operation: "ingest"}, {Method: "POST", Path: "/_ingest/pipeline/_simulate", Operation: "ingest"}, {Method: "POST", Path: "/_ingest/pipeline/{id}/_simulate", Operation: "ingest"}, {Method: "HEAD", Path: "/{index}/_mapping/{type}", Collection: "{index}", Operation: "mapping"}, {Method: "GET", Path: "/_mapping/field/{fields}", Operation: "mapping"}, {Method: "GET", Path: "/_mapping/{type}/field/{fields}", Operation: "mapping"}, {Method: "GET", Path: "/{index}/_mapping/field/{fields}", Collection: "{index}", Operation: "mapping"}, {Method: "GET", Path: "/{index}/_mapping/{type}/field/{fields}", Collection: "{index}", Operation: "mapping"}, {Method: "GET", Path: "/_mapping", Operation: "mapping"}, {Method: "GET", Path: "/_mapping/{type}", Operation: "mapping"}, {Method: "GET", Path: "/{index}/_mapping", Collection: "{index}", Operation: "mapping"}, {Method: "GET", Path: "/{index}/_mapping/{type}", Collection: "{index}", Operation: "mapping"}, {Method: "POST", Path: "/_mapping/{type}", Operation: "mapping"}, {Method: "POST", Path: "/{index}/_mapping/{type}", Collection: "{index}", Operation: "mapping"}, {Method: "POST", Path: "/{index}/{type}/_mapping", Collection: "{index}", Operation: "mapping"}, {Method: "POST", Path: "{index}/_mapping", Collection: "{index}", Operation: "mapping"}, {Method: "PUT", Path: "/_mapping/{type}", Operation: "mapping"}, {Method: "PUT", Path: "/{index}/_mapping/{type}", Collection: "{index}", Operation: "mapping"}, {Method: "PUT", Path: "/{index}/{type}/_mapping", Collection: "{index}", Operation: "mapping"}, {Method: "PUT", Path: "{index}/_mapping", Collection: "{index}", Operation: "mapping"}, {Method: "POST", Path: "/_mappings/{type}", Operation: "mappings"}, {Method: "POST", Path: "/{index}/_mappings/{type}", Collection: "{index}", Operation: "mappings"}, {Method: "POST", Path: "/{index}/{type}/_mappings", Collection: "{index}", Operation: "mappings"}, {Method: "POST", Path: "{index}/_mappings", Collection: "{index}", Operation: "mappings"}, {Method: "PUT", Path: "/_mappings/{type}", Operation: "mappings"}, {Method: "PUT", Path: "/{index}/_mappings/{type}", Collection: "{index}", Operation: "mappings"}, {Method: "PUT", Path: "/{index}/{type}/_mappings", Collection: "{index}", Operation: "mappings"}, {Method: "PUT", Path: "{index}/_mappings", Collection: "{index}", Operation: "mappings"}, {Method: "GET", Path: "/_mget", Operation: "mget"}, {Method: "GET", Path: "/{index}/_mget", Collection: "{index}", Operation: "mget"}, {Method: "GET", Path: "/{index}/{type}/_mget", Collection: "{index}", Operation: "mget"}, {Method: "POST", Path: "/_mget", Operation: "mget"}, {Method: "POST", Path: "/{index}/_mget", Collection: "{index}", Operation: "mget"}, {Method: "POST", Path: "/{index}/{type}/_mget", Collection: "{index}", Operation: "mget"}, {Method: "GET", Path: "/_msearch", Operation: "msearch"}, {Method: "GET", Path: "/{index}/_msearch", Collection: "{index}", Operation: "msearch"}, {Method: "GET", Path: "/{index}/{type}/_msearch", Collection: "{index}", Operation: "msearch"}, {Method: "POST", Path: "/_msearch", Operation: "msearch"}, {Method: "POST", Path: "/{index}/_msearch", Collection: "{index}", Operation: "msearch"}, {Method: "POST", Path: "/{index}/{type}/_msearch", Collection: "{index}", Operation: "msearch"}, {Method: "GET", Path: "/_msearch/template", Operation: "msearch"}, {Method: "GET", Path: "/{index}/_msearch/template", Collection: "{index}", Operation: "msearch"}, {Method: "GET", Path: "/{index}/{type}/_msearch/template", Collection: "{index}", Operation: "msearch"}, {Method: "POST", Path: "/_msearch/template", Operation: "msearch"}, {Method: "POST", Path: "/{index}/_msearch/template", Collection: "{index}", Operation: "msearch"}, {Method: "POST", Path: "/{index}/{type}/_msearch/template", Collection: "{index}", Operation: "msearch"}, {Method: "GET", Path: "/_mtermvectors", Operation: "mtermvectors"}, {Method: "GET", Path: "/{index}/_mtermvectors", Collection: "{index}", Operation: "mtermvectors"}, {Method: "GET", Path: "/{index}/{type}/_mtermvectors", Collection: "{index}", Operation: "mtermvectors"}, {Method: "POST", Path: "/_mtermvectors", Operation: "mtermvectors"}, {Method: "POST", Path: "/{index}/_mtermvectors", Collection: "{index}", Operation: "mtermvectors"}, {Method: "POST", Path: "/{index}/{type}/_mtermvectors", Collection: "{index}", Operation: "mtermvectors"}, {Method: "GET", Path: "/_nodes/hot_threads", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/hotthreads", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/hot_threads", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/hotthreads", Operation: "nodes"}, {Method: "GET", Path: "/_nodes", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/{metric}", Operation: "nodes"}, {Method: "POST", Path: "/_nodes/reload_secure_settings", Operation: "nodes"}, {Method: "POST", Path: "/_nodes/{node_id}/reload_secure_settings", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/stats", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/stats/{metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/stats/{metric}/{index_metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/stats", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/stats/{metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/stats/{metric}/{index_metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/usage", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/usage/{metric}", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/usage", Operation: "nodes"}, {Method: "GET", Path: "/_nodes/{node_id}/usage/{metric}", Operation: "nodes"}, {Method: "POST", Path: "/{index}/_open", Collection: "{index}", Operation: "open"}, {Method: "GET", Path: "/_rank_eval", Operation: "rank_eval"}, {Method: "GET", Path: "/{index}/_rank_eval", Collection: "{index}", Operation: "rank_eval"}, {Method: "POST", Path: "/_rank_eval", Operation: "rank_eval"}, {Method: "POST", Path: "/{index}/_rank_eval", Collection: "{index}", Operation: "rank_eval"}, {Method: "GET", Path: "/_recovery", Operation: "recovery"}, {Method: "GET", Path: "/{index}/_recovery", Collection: "{index}", Operation: "recovery"}, {Method: "GET", Path: "/_refresh", Operation: "refresh"}, {Method: "GET", Path: "/{index}/_refresh", Collection: "{index}", Operation: "refresh"}, {Method: "POST", Path: "/_refresh", Operation: "refresh"}, {Method: "POST", Path: "/{index}/_refresh", Collection: "{index}", Operation: "refresh"}, {Method: "POST", Path: "/_reindex", Operation: "reindex"}, {Method: "POST", Path: "/_reindex/{task_id}/_rethrottle", Operation: "reindex"}, {Method: "GET", Path: "/_remote/info", Operation: "remote"}, {Method: "GET", Path: "/_render/template", Operation: "render"}, {Method: "GET", Path: "/_render/template/{id}", Operation: "render"}, {Method: "POST", Path: "/_render/template", Operation: "render"}, {Method: "POST", Path: "/_render/template/{id}", Operation: "render"}, {Method: "POST", Path: "/{alias}/_rollover", Operation: "rollover", Collection: "{alias}"}, {Method: "POST", Path: "/{alias}/_rollover/{new_index}", Operation: "rollover", Collection: "{alias}"}, {Method: "DELETE", Path: "/_scripts/{id}", Operation: "scripts"}, {Method: "GET", Path: "/_scripts/{id}", Operation: "scripts"}, {Method: "POST", Path: "/_scripts/{id}", Operation: "scripts"}, {Method: "POST", Path: "/_scripts/{id}/{context}", Operation: "scripts"}, {Method: "PUT", Path: "/_scripts/{id}", Operation: "scripts"}, {Method: "PUT", Path: "/_scripts/{id}/{context}", Operation: "scripts"}, {Method: "GET", Path: "/_scripts/painless/_execute", Operation: "scripts"}, {Method: "POST", Path: "/_scripts/painless/_execute", Operation: "scripts"}, {Method: "DELETE", Path: "/_search/scroll", Operation: "clear_scroll"}, {Method: "DELETE", Path: "/_search/scroll/{scroll_id}", Operation: "clear_scroll"}, {Method: "GET", Path: "/_search/scroll", Operation: "scroll"}, {Method: "GET", Path: "/_search/scroll/{scroll_id}", Operation: "scroll"}, {Method: "POST", Path: "/_search/scroll", Operation: "scroll"}, {Method: "POST", Path: "/_search/scroll/{scroll_id}", Operation: "scroll"}, {Method: "GET", Path: "/_search", Operation: "search"}, {Method: "GET", Path: "/{index}/_search", Collection: "{index}", Operation: "search"}, {Method: "GET", Path: "/{index}/{type}/_search", Collection: "{index}", Operation: "search"}, {Method: "POST", Path: "/_search", Operation: "search"}, {Method: "POST", Path: "/{index}/_search", Collection: "{index}", Operation: "search"}, {Method: "POST", Path: "/{index}/{type}/_search", Collection: "{index}", Operation: "search"}, {Method: "GET", Path: "/_search/template", Operation: "search_template"}, {Method: "GET", Path: "/{index}/_search/template", Collection: "{index}", Operation: "search_template"}, {Method: "GET", Path: "/{index}/{type}/_search/template", Collection: "{index}", Operation: "search_template"}, {Method: "POST", Path: "/_search/template", Operation: "search_template"}, {Method: "POST", Path: "/{index}/_search/template", Collection: "{index}", Operation: "search_template"}, {Method: "POST", Path: "/{index}/{type}/_search/template", Collection: "{index}", Operation: "search_template"}, {Method: "GET", Path: "/_search_shards", Operation: "search_shards"}, {Method: "GET", Path: "/{index}/_search_shards", Collection: "{index}", Operation: "search_shards"}, {Method: "POST", Path: "/_search_shards", Operation: "search_shards"}, {Method: "POST", Path: "/{index}/_search_shards", Collection: "{index}", Operation: "search_shards"}, {Method: "GET", Path: "/_segments", Operation: "segments"}, {Method: "GET", Path: "/{index}/_segments", Collection: "{index}", Operation: "segments"}, {Method: "GET", Path: "/_settings", Operation: "settings"}, {Method: "GET", Path: "/_settings/{name}", Operation: "settings"}, {Method: "GET", Path: "/{index}/_settings", Collection: "{index}", Operation: "settings"}, {Method: "GET", Path: "/{index}/_settings/{name}", Collection: "{index}", Operation: "settings"}, {Method: "PUT", Path: "/_settings", Operation: "settings"}, {Method: "PUT", Path: "/{index}/_settings", Collection: "{index}", Operation: "settings"}, {Method: "GET", Path: "/_shard_stores", Operation: "shard_stores"}, {Method: "GET", Path: "/{index}/_shard_stores", Collection: "{index}", Operation: "shard_stores"}, {Method: "POST", Path: "/{index}/_shrink/{target}", Collection: "{index}", Operation: "shrink"}, {Method: "PUT", Path: "/{index}/_shrink/{target}", Collection: "{index}", Operation: "shrink"}, {Method: "POST", Path: "/_snapshot/{repository}/_cleanup", Operation: "snapshot"}, {Method: "POST", Path: "/_snapshot/{repository}/{snapshot}", Operation: "snapshot"}, {Method: "PUT", Path: "/_snapshot/{repository}/{snapshot}", Operation: "snapshot"}, {Method: "POST", Path: "/_snapshot/{repository}", Operation: "snapshot"}, {Method: "PUT", Path: "/_snapshot/{repository}", Operation: "snapshot"}, {Method: "DELETE", Path: "/_snapshot/{repository}/{snapshot}", Operation: "snapshot"}, {Method: "DELETE", Path: "/_snapshot/{repository}", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot/{repository}/{snapshot}", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot/{repository}", Operation: "snapshot"}, {Method: "POST", Path: "/_snapshot/{repository}/{snapshot}/_restore", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot/_status", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot/{repository}/_status", Operation: "snapshot"}, {Method: "GET", Path: "/_snapshot/{repository}/{snapshot}/_status", Operation: "snapshot"}, {Method: "POST", Path: "/_snapshot/{repository}/_verify", Operation: "snapshot"}, {Method: "HEAD", Path: "/{index}/_source/{id}", Collection: "{index}", Operation: "source"}, {Method: "HEAD", Path: "/{index}/{type}/{id}/_source", Collection: "{index}", Operation: "source"}, {Method: "GET", Path: "/{index}/_source/{id}", Collection: "{index}", Operation: "source"}, {Method: "GET", Path: "/{index}/{type}/{id}/_source", Collection: "{index}", Operation: "source"}, {Method: "POST", Path: "/{index}/_split/{target}", Collection: "{index}", Operation: "split"}, {Method: "PUT", Path: "/{index}/_split/{target}", Collection: "{index}", Operation: "split"}, {Method: "GET", Path: "/_stats", Operation: "stats"}, {Method: "GET", Path: "/_stats/{metric}", Operation: "stats"}, {Method: "GET", Path: "/{index}/_stats", Collection: "{index}", Operation: "stats"}, {Method: "GET", Path: "/{index}/_stats/{metric}", Collection: "{index}", Operation: "stats"}, {Method: "POST", Path: "/_tasks/_cancel", Operation: "tasks"}, {Method: "POST", Path: "/_tasks/{task_id}/_cancel", Operation: "tasks"}, {Method: "GET", Path: "/_tasks/{task_id}", Operation: "tasks"}, {Method: "GET", Path: "/_tasks", Operation: "tasks"}, {Method: "DELETE", Path: "/_template/{name}", Operation: "template"}, {Method: "HEAD", Path: "/_template/{name}", Operation: "template"}, {Method: "GET", Path: "/_template", Operation: "template"}, {Method: "GET", Path: "/_template/{name}", Operation: "template"}, {Method: "POST", Path: "/_template/{name}", Operation: "template"}, {Method: "PUT", Path: "/_template/{name}", Operation: "template"}, {Method: "GET", Path: "/{index}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "GET", Path: "/{index}/_termvectors/{id}", Collection: "{index}", Operation: "termvectors"}, {Method: "GET", Path: "/{index}/{type}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "GET", Path: "/{index}/{type}/{id}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "POST", Path: "/{index}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "POST", Path: "/{index}/_termvectors/{id}", Collection: "{index}", Operation: "termvectors"}, {Method: "POST", Path: "/{index}/{type}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "POST", Path: "/{index}/{type}/{id}/_termvectors", Collection: "{index}", Operation: "termvectors"}, {Method: "POST", Path: "/{index}/_update/{id}", Collection: "{index}", Operation: "update"}, {Method: "POST", Path: "/{index}/{type}/{id}/_update", Collection: "{index}", Operation: "update"}, {Method: "POST", Path: "/{index}/_update_by_query", Collection: "{index}", Operation: "update_by_query"}, {Method: "POST", Path: "/{index}/{type}/_update_by_query", Collection: "{index}", Operation: "update_by_query"}, {Method: "POST", Path: "/_update_by_query/{task_id}/_rethrottle", Operation: "update_by_query"}, {Method: "GET", Path: "/_upgrade", Operation: "upgrade"}, {Method: "GET", Path: "/{index}/_upgrade", Collection: "{index}", Operation: "upgrade"}, {Method: "POST", Path: "/_upgrade", Operation: "upgrade"}, {Method: "POST", Path: "/{index}/_upgrade", Collection: "{index}", Operation: "upgrade"}, {Method: "GET", Path: "/_validate/query", Operation: "validate"}, {Method: "GET", Path: "/{index}/_validate/query", Collection: "{index}", Operation: "validate"}, {Method: "GET", Path: "/{index}/{type}/_validate/query", Collection: "{index}", Operation: "validate"}, {Method: "POST", Path: "/_validate/query", Operation: "validate"}, {Method: "POST", Path: "/{index}/_validate/query", Collection: "{index}", Operation: "validate"}, {Method: "POST", Path: "/{index}/{type}/_validate/query", Collection: "{index}", Operation: "validate"}, } for _, tc := range testcases { r := &http.Request{ URL: &url.URL{ Path: tc.Path, }, Method: tc.Method, } segment := parseRequest(r) if segment.Operation != tc.Operation { t.Error("wrong operation", tc.Method, tc.Path, segment.Operation, tc.Operation) } if segment.Collection != tc.Collection { t.Error("wrong operation", tc.Method, tc.Path, segment.Collection, tc.Collection) } } } var ( errSomething = errors.New("something went wrong") ) type roundTripperFunc func(*http.Request) (*http.Response, error) func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return fn(r) } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(nil, integrationsupport.ConfigFullTraces) } func TestInfo(t *testing.T) { app := createTestApp() txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client, err := elasticsearch.NewClient(elasticsearch.Config{ Transport: NewRoundTripper(roundTripperFunc(func(r *http.Request) (*http.Response, error) { return nil, errSomething })), }) if err != nil { t.Fatal(err) } _, err = client.Info(client.Info.WithContext(ctx)) if err != errSomething { t.Fatal(err) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, {Name: "OtherTransaction/Go/txnName"}, {Name: "OtherTransaction/all"}, {Name: "OtherTransactionTotalTime"}, {Name: "Datastore/all", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/Elasticsearch/all", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/Elasticsearch/allOther", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/operation/Elasticsearch/info", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/operation/Elasticsearch/info", Scope: "OtherTransaction/Go/txnName", Forced: nil, Data: nil}, }) } func TestSearch(t *testing.T) { app := createTestApp() txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client, err := elasticsearch.NewClient(elasticsearch.Config{ Transport: NewRoundTripper(roundTripperFunc(func(r *http.Request) (*http.Response, error) { return nil, errSomething })), }) if err != nil { t.Fatal(err) } body := `{"query":{"match":{"title":"test"}}}` _, err = client.Search( client.Search.WithContext(ctx), client.Search.WithIndex("myindex"), client.Search.WithBody(strings.NewReader(body)), ) if err != errSomething { t.Fatal(err) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, {Name: "OtherTransaction/Go/txnName"}, {Name: "OtherTransaction/all"}, {Name: "OtherTransactionTotalTime"}, {Name: "Datastore/all"}, {Name: "Datastore/allOther"}, {Name: "Datastore/Elasticsearch/all"}, {Name: "Datastore/Elasticsearch/allOther"}, {Name: "Datastore/operation/Elasticsearch/info"}, {Name: "Datastore/operation/Elasticsearch/info", Scope: "OtherTransaction/Go/txnName", Forced: nil, Data: nil}, }) } func TestInfoRequest(t *testing.T) { // Test that the instrumentation works as expected when the Do() // request pattern is used. app := createTestApp() txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client, err := elasticsearch.NewClient(elasticsearch.Config{ Transport: NewRoundTripper(roundTripperFunc(func(r *http.Request) (*http.Response, error) { return nil, errSomething })), }) if err != nil { t.Fatal(err) } req := esapi.InfoRequest{} _, err = req.Do(ctx, client) if err != errSomething { t.Fatal(err) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, {Name: "OtherTransaction/Go/txnName"}, {Name: "OtherTransaction/all"}, {Name: "OtherTransactionTotalTime"}, {Name: "Datastore/all", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/Elasticsearch/all", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/Elasticsearch/allOther", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/operation/Elasticsearch/info", Scope: "", Forced: nil, Data: nil}, {Name: "Datastore/operation/Elasticsearch/info", Scope: "OtherTransaction/Go/txnName", Forced: nil, Data: nil}, }) } go-agent-3.42.0/v3/integrations/nrfasthttp/000077500000000000000000000000001510742411500205345ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfasthttp/LICENSE.txt000066400000000000000000000264501510742411500223660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrfasthttp/examples/000077500000000000000000000000001510742411500223525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfasthttp/examples/client-fasthttp/000077500000000000000000000000001510742411500254635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod000066400000000000000000000005041510742411500265700ustar00rootroot00000000000000module client-example go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 github.com/valyala/fasthttp v1.49.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 => ../../ replace github.com/newrelic/go-agent/v3 => ../../../.. go-agent-3.42.0/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go000066400000000000000000000025301510742411500267360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrfasthttp" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" ) func doRequest(txn *newrelic.Transaction) error { req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://localhost:8080/hello") req.Header.SetMethod("GET") seg := nrfasthttp.StartExternalSegment(txn, req) defer seg.End() err := fasthttp.Do(req, resp) if err != nil { return err } fmt.Println("Response Code is ", resp.StatusCode()) return nil } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Client App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } if err != nil { fmt.Println(err) os.Exit(1) } txn := app.StartTransaction("client-txn") err = doRequest(txn) if err != nil { txn.NoticeError(err) } txn.End() // Shut down the application to flush data to New Relic. app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrfasthttp/examples/server-fasthttp/000077500000000000000000000000001510742411500255135ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod000066400000000000000000000005041510742411500266200ustar00rootroot00000000000000module server-example go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 github.com/valyala/fasthttp v1.49.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 => ../../ replace github.com/newrelic/go-agent/v3 => ../../../.. go-agent-3.42.0/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go000066400000000000000000000026641510742411500267760ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrfasthttp" "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" ) func index(ctx *fasthttp.RequestCtx) { ctx.WriteString("Hello World") } func noticeError(ctx *fasthttp.RequestCtx) { ctx.WriteString("noticing an error") txn := ctx.UserValue("transaction").(*newrelic.Transaction) txn.NoticeError(errors.New("my error message")) } func main() { // Initialize New Relic app, err := newrelic.NewApplication( newrelic.ConfigAppName("FastHTTP App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if err != nil { fmt.Println(err) return } if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } _, helloRoute := nrfasthttp.WrapHandleFunc(app, "/hello", index) _, errorRoute := nrfasthttp.WrapHandleFunc(app, "/error", noticeError) handler := func(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) method := string(ctx.Method()) switch { case method == "GET" && path == "/hello": helloRoute(ctx) case method == "GET" && path == "/error": errorRoute(ctx) } } // Start the server with the instrumented handler fasthttp.ListenAndServe(":8080", handler) } go-agent-3.42.0/v3/integrations/nrfasthttp/go.mod000066400000000000000000000003261510742411500216430ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrfasthttp go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/valyala/fasthttp v1.49.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrfasthttp/instrumentation.go000066400000000000000000000060411510742411500243270ustar00rootroot00000000000000package nrfasthttp import ( "net/http" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpadaptor" ) func init() { internal.TrackUsage("integration", "framework", "fasthttp") } type fasthttpWrapperResponse struct { ctx *fasthttp.RequestCtx } func (rw fasthttpWrapperResponse) Header() http.Header { hdrs := http.Header{} rw.ctx.Request.Header.VisitAll(func(key, value []byte) { hdrs.Add(string(key), string(value)) }) return hdrs } func (rw fasthttpWrapperResponse) Write(b []byte) (int, error) { return rw.ctx.Write(b) } func (rw fasthttpWrapperResponse) WriteHeader(code int) { rw.ctx.SetStatusCode(code) } func (rw fasthttpWrapperResponse) Body() string { body := rw.ctx.Response.Body() return string(body) } // WrapHandleFunc wrapps a fasthttp handler function for automatic instrumentation func WrapHandleFunc(app *newrelic.Application, pattern string, handler func(*fasthttp.RequestCtx), options ...newrelic.TraceOption) (string, func(*fasthttp.RequestCtx)) { // add the wrapped function to the trace options as the source code reference point // (to the beginning of the option list, so that the user can override this) p, h := WrapHandle(app, pattern, fasthttp.RequestHandler(handler), options...) return p, func(ctx *fasthttp.RequestCtx) { h(ctx) } } // WrapHandle wraps a fasthttp request handler for automatic instrumentation func WrapHandle(app *newrelic.Application, pattern string, handler fasthttp.RequestHandler, options ...newrelic.TraceOption) (string, fasthttp.RequestHandler) { if app == nil { return pattern, handler } if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", pattern, "*", internal.HandlerName(handler)) } // add the wrapped function to the trace options as the source code reference point // (but only if we know we're collecting CLM for this transaction and the user didn't already // specify a different code location explicitly). return pattern, func(ctx *fasthttp.RequestCtx) { cache := newrelic.NewCachedCodeLocation() txnOptionList := newrelic.AddCodeLevelMetricsTraceOptions(app, options, cache, handler) method := string(ctx.Method()) path := string(ctx.Path()) txn := app.StartTransaction(method+" "+path, txnOptionList...) ctx.SetUserValue("transaction", txn) defer txn.End() r := &http.Request{} fasthttpadaptor.ConvertRequest(ctx, r, true) resp := fasthttpWrapperResponse{ctx: ctx} if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, pattern) } txn.SetWebResponse(resp) txn.SetWebRequestHTTP(r) handler(ctx) if newrelic.IsSecurityAgentPresent() { header := resp.Header() ctx.Response.Header.VisitAllCookie(func(key, value []byte) { header.Add("Set-Cookie", string(value)) }) newrelic.GetSecurityAgentInterface().SendEvent("INBOUND_WRITE", resp.Body(), header) newrelic.GetSecurityAgentInterface().SendEvent("INBOUND_RESPONSE_CODE", ctx.Response.StatusCode()) } } } go-agent-3.42.0/v3/integrations/nrfasthttp/instrumentation_test.go000066400000000000000000000042061510742411500253670ustar00rootroot00000000000000package nrfasthttp import ( "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" ) type myError struct{} func (e myError) Error() string { return "my msg" } func myErrorHandlerFastHTTP(ctx *fasthttp.RequestCtx) { ctx.WriteString("noticing an error") txn := ctx.UserValue("transaction").(*newrelic.Transaction) txn.NoticeError(myError{}) } func TestWrapHandleFastHTTPFunc(t *testing.T) { singleCount := []float64{1, 0, 0, 0, 0, 0, 0} app := createTestApp(true) _, wrappedHandler := WrapHandleFunc(app.Application, "/hello", myErrorHandlerFastHTTP) if wrappedHandler == nil { t.Error("Error when creating a wrapped handler") } ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/hello") wrappedHandler(ctx) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/GET /hello", Msg: "my msg", Klass: "nrfasthttp.myError", }}) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, }) } go-agent-3.42.0/v3/integrations/nrfasthttp/segment.go000066400000000000000000000037151510742411500225330ustar00rootroot00000000000000package nrfasthttp import ( "net/http" "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpadaptor" ) // StartExternalSegment automatically creates and fills out a New Relic external segment for a given // fasthttp request object. This function will accept either a fasthttp.Request or a fasthttp.RequestContext // object as the request argument. func StartExternalSegment(txn *newrelic.Transaction, request any) *newrelic.ExternalSegment { var secureAgentEvent any var ctx *fasthttp.RequestCtx switch reqObject := request.(type) { case *fasthttp.RequestCtx: ctx = reqObject case *fasthttp.Request: ctx = &fasthttp.RequestCtx{} reqObject.CopyTo(&ctx.Request) default: return nil } if nil == txn { txn = transactionFromRequestContext(ctx) } req := &http.Request{} fasthttpadaptor.ConvertRequest(ctx, req, true) s := &newrelic.ExternalSegment{ StartTime: txn.StartSegmentNow(), Request: req, } if newrelic.IsSecurityAgentPresent() { secureAgentEvent = newrelic.GetSecurityAgentInterface().SendEvent("OUTBOUND", request) s.SetSecureAgentEvent(secureAgentEvent) } if request != nil && req.Header != nil { for key, values := range s.GetOutboundHeaders() { for _, value := range values { req.Header.Set(key, value) } } if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().DistributedTraceHeaders(req, secureAgentEvent) } for k, values := range req.Header { for _, value := range values { ctx.Request.Header.Set(k, value) } } } return s } // FromContext extracts a transaction pointer from a fasthttp.RequestContext object func FromContext(ctx *fasthttp.RequestCtx) *newrelic.Transaction { return transactionFromRequestContext(ctx) } func transactionFromRequestContext(ctx *fasthttp.RequestCtx) *newrelic.Transaction { if nil != ctx { txn := ctx.UserValue("transaction").(*newrelic.Transaction) return txn } return nil } go-agent-3.42.0/v3/integrations/nrfasthttp/segment_test.go000066400000000000000000000037541510742411500235750ustar00rootroot00000000000000package nrfasthttp import ( "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/valyala/fasthttp" ) func createTestApp(dt bool) integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigDistributedTracerEnabled(dt)) } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() } func TestExternalSegment(t *testing.T) { app := createTestApp(false) txn := app.StartTransaction("myTxn") resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) ctx := &fasthttp.RequestCtx{Request: fasthttp.Request{}} ctx.Request.SetRequestURI("http://localhost:8080/hello") ctx.Request.Header.SetMethod("GET") seg := StartExternalSegment(txn, ctx) defer seg.End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) } func TestExternalSegmentRequest(t *testing.T) { app := createTestApp(false) txn := app.StartTransaction("myTxn") req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://localhost:8080/hello") req.Header.SetMethod("GET") seg := StartExternalSegment(txn, req) defer seg.End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) } go-agent-3.42.0/v3/integrations/nrfiber/000077500000000000000000000000001510742411500177665ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfiber/LICENSE.txt000066400000000000000000000264501510742411500216200ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrfiber/README.md000066400000000000000000000007041510742411500212460ustar00rootroot00000000000000# v3/integrations/nrfiber [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrfiber?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrfiber) Package `nrfiber` instruments https://github.com/gofiber/fiber applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrfiber" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrfiber). go-agent-3.42.0/v3/integrations/nrfiber/example/000077500000000000000000000000001510742411500214215ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrfiber/example/main.go000066400000000000000000000037651510742411500227070ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/gofiber/fiber/v2" "github.com/newrelic/go-agent/v3/integrations/nrfiber" "github.com/newrelic/go-agent/v3/newrelic" ) func v1login(c *fiber.Ctx) { c.WriteString("v1 login") } func v1submit(c *fiber.Ctx) { c.WriteString("v1 submit") } func v1read(c *fiber.Ctx) { c.WriteString("v1 read") } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("fiber App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigCodeLevelMetricsEnabled(true), ) if nil != err { fmt.Println(err) os.Exit(1) } router := fiber.New() router.Use(nrfiber.Middleware(app)) // 404 handler router.Get("/404", func(c *fiber.Ctx) error { c.SendStatus(404) c.WriteString("returning 404") return nil }) // router.Get("/change", func(c *fiber.Ctx) error { c.SendStatus(404) c.SendStatus(200) c.WriteString("actually ok!") return nil }) // Headers router.Get("/headers", func(c *fiber.Ctx) error { // Since fiber.Response buffers the response code, response headers // can be set afterwards. c.SendStatus(200) c.Response().Header.Set("X-Custom", "custom value") c.SendString(`{"zip":"zap"}`) return nil }) router.Get("/txn", func(c *fiber.Ctx) error { txn := nrfiber.Transaction(c.Context()) txn.SetName("custom-name") c.WriteString("changed the name of the transaction!") return nil }) // Since the handler function name is used as the transaction name, // anonymous functions do not get usefully named. We encourage // transforming anonymous functions into named functions. router.Get("/anon", func(c *fiber.Ctx) error { return c.SendString("anonymous function handler") }) v1 := router.Group("/v1") v1.Get("/login", func(c *fiber.Ctx) error { v1login(c) return nil }) v1.Get("/submit", func(c *fiber.Ctx) error { v1submit(c) return nil }) v1.Get("/read", func(c *fiber.Ctx) error { v1read(c) return nil }) router.Listen(":8000") } go-agent-3.42.0/v3/integrations/nrfiber/example/main_test.go000066400000000000000000000060121510742411500237320ustar00rootroot00000000000000package main_test import ( "io" "net/http/httptest" "testing" "github.com/gofiber/fiber/v2" "github.com/newrelic/go-agent/v3/integrations/nrfiber" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func setupTestApp(t *testing.T) *fiber.App { app := integrationsupport.NewBasicTestApp().Application if app == nil { t.Fatal("Failed to create New Relic application") } // Create a new Fiber app router := fiber.New() router.Use(nrfiber.Middleware(app)) return router } func TestRoutes404(t *testing.T) { app := setupTestApp(t) app.Get("/404", func(c *fiber.Ctx) error { c.SendStatus(404) return c.SendString("returning 404") }) req := httptest.NewRequest("GET", "/404", nil) resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != 404 { t.Errorf("Expected status code 404, got %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) if string(body) != "returning 404" { t.Errorf("Expected body 'returning 404', got '%s'", string(body)) } } func TestRouteStatusChange(t *testing.T) { app := setupTestApp(t) app.Get("/change", func(c *fiber.Ctx) error { c.SendStatus(404) c.SendStatus(200) return c.SendString("actually ok!") }) req := httptest.NewRequest("GET", "/change", nil) resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Errorf("Expected final status code 200, got %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) if string(body) != "actually ok!" { t.Errorf("Expected body 'actually ok!', got '%s'", string(body)) } } func TestCustomHeaders(t *testing.T) { app := setupTestApp(t) app.Get("/headers", func(c *fiber.Ctx) error { c.SendStatus(200) c.Response().Header.Set("X-Custom", "custom value") return c.SendString(`{"zip":"zap"}`) }) req := httptest.NewRequest("GET", "/headers", nil) resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.Header.Get("X-Custom") != "custom value" { t.Errorf("Expected X-Custom header 'custom value', got '%s'", resp.Header.Get("X-Custom")) } body, _ := io.ReadAll(resp.Body) if string(body) != `{"zip":"zap"}` { t.Errorf("Expected body '{\"zip\":\"zap\"}', got '%s'", string(body)) } } func TestV1GroupRoutes(t *testing.T) { app := setupTestApp(t) v1 := app.Group("/v1") v1.Get("/login", func(c *fiber.Ctx) error { return c.SendString("login") }) v1.Get("/submit", func(c *fiber.Ctx) error { return c.SendString("submit") }) v1.Get("/read", func(c *fiber.Ctx) error { return c.SendString("read") }) paths := []string{"/v1/login", "/v1/submit", "/v1/read"} expected := []string{"login", "submit", "read"} for i, path := range paths { req := httptest.NewRequest("GET", path, nil) resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Errorf("Expected status code 200 for %s, got %d", path, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) if string(body) != expected[i] { t.Errorf("Expected body '%s' for %s, got '%s'", expected[i], path, string(body)) } } } go-agent-3.42.0/v3/integrations/nrfiber/go.mod000066400000000000000000000004351510742411500210760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrfiber go 1.24 require ( github.com/gofiber/fiber/v2 v2.52.9 github.com/newrelic/go-agent/v3 v3.42.0 github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.51.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrfiber/nfiber_test.go000066400000000000000000000301531510742411500226230ustar00rootroot00000000000000package nrfiber import ( "io" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) // TestMiddleware_NoNewRelicApp ensures requests proceed normally if no New Relic app is provided func TestMiddleware_NoNewRelicApp(t *testing.T) { fiberApp := fiber.New() fiberApp.Use(Middleware(nil)) fiberApp.Get("/no-nr", func(c *fiber.Ctx) error { return c.SendString("No NR App") }) // Simulate a request req, err := http.NewRequest("GET", "/no-nr", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := fiberApp.Test(req, -1) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) } // TestMiddleware_Success checks if the middleware correctly creates a New Relic transaction func TestMiddleware_Success(t *testing.T) { // Create a test New Relic application app := integrationsupport.NewBasicTestApp() // Initialize Fiber app with New Relic middleware fiberApp := fiber.New() fiberApp.Use(Middleware(app.Application)) // Define a sample route fiberApp.Get("/test", func(c *fiber.Ctx) error { return c.SendString("Hello, World!") }) // Simulate a request req, err := http.NewRequest("GET", "/test", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := fiberApp.Test(req, -1) // Verify that no error occurred require.Nil(t, err) // Read the response body body, _ := io.ReadAll(resp.Body) if respBody := string(body); respBody != "Hello, World!" { t.Error("wrong response body", respBody) } // Assertions require.Equal(t, http.StatusOK, resp.StatusCode) // Check if the transaction was created app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /test", IsWeb: true, UnknownCaller: true, }) } // TestMiddleware_AnonymousFunctions checks if the middleware correctly handles anonymous functions func TestMiddleware_AnonymousFunctions(t *testing.T) { app := integrationsupport.NewBasicTestApp() fiberApp := fiber.New() fiberApp.Use(Middleware(app.Application)) fiberApp.Get("/helloAnon", func(c *fiber.Ctx) error { return c.SendString("Hello, anon!") }) req, err := http.NewRequest("GET", "/helloAnon", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := fiberApp.Test(req, -1) // Verify that no error occurred require.Nil(t, err) // Read the response body body, _ := io.ReadAll(resp.Body) if respBody := string(body); respBody != "Hello, anon!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /helloAnon", IsWeb: true, UnknownCaller: true, }) } // TestMiddleware_ErrorHandling checks if the middleware captures errors correctly func TestMiddleware_ErrorHandling(t *testing.T) { app := integrationsupport.NewBasicTestApp() fiberApp := fiber.New() fiberApp.Use(Middleware(app.Application)) // Define a route that returns an error fiberApp.Get("/error", func(c *fiber.Ctx) error { return fiber.ErrInternalServerError }) // Simulate a request req, err := http.NewRequest("GET", "/error", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := fiberApp.Test(req, -1) // Verify that no error occurred require.Nil(t, err) // Assertions assert.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) // Ensure the error is noticed in New Relic app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/GET /error", Msg: "Internal Server Error", }}) } // TestWrapHandler verifies that WrapHandler correctly wraps an existing handler func TestWrapHandler(t *testing.T) { app := integrationsupport.NewBasicTestApp() fiberApp := fiber.New() wrappedHandler := WrapHandler(app.Application, "/wrapped", func(c *fiber.Ctx) error { return c.SendString("Wrapped Handler") }) fiberApp.Get("/wrapped", wrappedHandler) // Simulate a request req, err := http.NewRequest("GET", "/wrapped", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := fiberApp.Test(req, -1) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) // Ensure the transaction was correctly named // Check if the transaction was created app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /wrapped", IsWeb: true, UnknownCaller: true, }) } // Test_GetTransactionName tests the getTransactionName function func Test_GetTransactionName(t *testing.T) { tests := []struct { name string path string method string expected string }{ { name: "Root path", path: "/", method: "GET", expected: "GET /", }, { name: "API path", path: "/api/users", method: "POST", expected: "POST /api/users", }, { name: "Empty path defaults to root", path: "", method: "DELETE", expected: "DELETE /", }, { name: "Path with query parameters", path: "/search?q=fiber", method: "GET", expected: "GET /search", }, { name: "Path with parameters", path: "/products/:id", method: "PUT", expected: "PUT /products/:id", }, { name: "Path with multiple parameters", path: "/users/:id/posts/:postID", method: "GET", expected: "GET /users/:id/posts/:postID", }, { name: "Path with trailing slash", path: "/products/", method: "PUT", expected: "PUT /products/", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a new Fiber app and request for testing app := fiber.New() fctx := &fasthttp.RequestCtx{} fctx.Request.Header.SetMethod(tt.method) fctx.Request.SetRequestURI(tt.path) ctx := app.AcquireCtx(fctx) ctx.Request().SetRequestURI(tt.path) ctx.Request().Header.SetMethod(tt.method) // Get the transaction name result := getTransactionName(ctx) // Verify the result assert.Equal(t, tt.expected, result) }) } } // Test_FastHeaderResponseWriter test the different header operations func Test_FastHeaderResponseWriter(t *testing.T) { // Setup app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) t.Run("implements http.ResponseWriter", func(t *testing.T) { // Verify that our implementation satisfies the http.ResponseWriter interface var _ http.ResponseWriter = &fastHeaderResponseWriter{} }) t.Run("header operations", func(t *testing.T) { writer := newFastHeaderResponseWriter(ctx.Response()) // Test adding headers writer.Header().Add("X-Test-Header", "value1") writer.Header().Set("X-Single-Header", "single-value") // Verify headers were stored in the wrapper assert.Equal(t, []string{"value1"}, writer.header["X-Test-Header"]) assert.Equal(t, []string{"single-value"}, writer.header["X-Single-Header"]) // Verify headers aren't yet in the actual response assert.Equal(t, "", string(ctx.Response().Header.Peek("X-Test-Header"))) // Apply headers and verify they're in the response writer.applyHeaders() assert.Equal(t, "value1", string(ctx.Response().Header.Peek("X-Test-Header"))) assert.Equal(t, "single-value", string(ctx.Response().Header.Peek("X-Single-Header"))) }) t.Run("status code handling", func(t *testing.T) { writer := newFastHeaderResponseWriter(ctx.Response()) // Default status should be 200 OK assert.Equal(t, http.StatusOK, writer.statusCode) // Set status code writer.WriteHeader(http.StatusNotFound) // Check internal tracking assert.Equal(t, http.StatusNotFound, writer.statusCode) // Check fiber response status assert.Equal(t, http.StatusNotFound, ctx.Response().StatusCode()) }) t.Run("write operation", func(t *testing.T) { writer := newFastHeaderResponseWriter(ctx.Response()) // The Write method is a no-op but we should test it returns expected values n, err := writer.Write([]byte("test")) assert.Equal(t, 0, n) assert.NoError(t, err) }) t.Run("integration with fiber response", func(t *testing.T) { writer := newFastHeaderResponseWriter(ctx.Response()) // Set headers via our wrapper writer.Header().Set("Content-Type", "application/json") writer.Header().Set("X-API-Key", "secret-key") // Set status writer.WriteHeader(http.StatusCreated) // Apply headers writer.applyHeaders() // Check that fiber response has the correct values assert.Equal(t, http.StatusCreated, ctx.Response().StatusCode()) assert.Equal(t, "application/json", string(ctx.Response().Header.Peek("Content-Type"))) assert.Equal(t, "secret-key", string(ctx.Response().Header.Peek("X-API-Key"))) }) } // This test specifically verifies the behavior when multiple values are set for a header func Test_MultiValueHeaders(t *testing.T) { app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) writer := newFastHeaderResponseWriter(ctx.Response()) // Add multiple Set-Cookie headers (common use case for multi-value headers) writer.Header().Add("Set-Cookie", "cookie1=value1; Path=/") writer.Header().Add("Set-Cookie", "cookie2=value2; Path=/") // Apply headers writer.applyHeaders() // Fiber should handle multiple values properly for Set-Cookie // Get all Set-Cookie headers cookies := []string{} ctx.Response().Header.VisitAllCookie(func(key, value []byte) { cookies = append(cookies, string(value)) }) // Should have two cookies assert.Len(t, cookies, 2) assert.Contains(t, cookies, "cookie1=value1; Path=/") assert.Contains(t, cookies, "cookie2=value2; Path=/") } // This test verifies that our response writer correctly interacts with a New Relic transaction func Test_FastHeaderResponseWriterWithNRTransaction(t *testing.T) { // This is a mock test to demonstrate interaction with NR transaction // In a real test, you would use a mock for the New Relic transaction app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) writer := newFastHeaderResponseWriter(ctx.Response()) // Set custom headers writer.Header().Set("Content-Type", "application/json") writer.Header().Set("X-Custom-Header", "custom-value") // Set a non-200 status code writer.WriteHeader(http.StatusBadRequest) // Apply the headers writer.applyHeaders() // Verify response state assert.Equal(t, http.StatusBadRequest, writer.statusCode) assert.Equal(t, http.StatusBadRequest, ctx.Response().StatusCode()) assert.Equal(t, "application/json", string(ctx.Response().Header.Peek("Content-Type"))) assert.Equal(t, "custom-value", string(ctx.Response().Header.Peek("X-Custom-Header"))) } // This test verifies that header manipulations after WriteHeader still work func Test_HeadersAfterWriteHeader(t *testing.T) { app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) writer := newFastHeaderResponseWriter(ctx.Response()) // Set status code first writer.WriteHeader(http.StatusAccepted) // Then manipulate headers writer.Header().Set("X-Late-Header", "late-value") // Apply headers writer.applyHeaders() // Verify everything was set correctly assert.Equal(t, http.StatusAccepted, ctx.Response().StatusCode()) assert.Equal(t, "late-value", string(ctx.Response().Header.Peek("X-Late-Header"))) } // Test_RouterGroup tests the router group functionality func Test_RouterGroup(t *testing.T) { // Create a new Fiber app and router group for testing app := integrationsupport.NewBasicTestApp() router := fiber.New() router.Use(Middleware(app.Application)) group := router.Group("/group") group.Get("/hello", func(c *fiber.Ctx) error { return c.SendString("hello response") }) // Simulate a request req, err := http.NewRequest("GET", "/group/hello", nil) if err != nil { t.Fatal(err) } // Make a test request resp, err := router.Test(req, -1) // Verify that no error occurred require.Nil(t, err) // Read the response body body, _ := io.ReadAll(resp.Body) if respBody := string(body); respBody != "hello response" { t.Error("wrong response body", respBody) } // Ensure the transaction was correctly named app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /group/hello", IsWeb: true, UnknownCaller: true, }) } go-agent-3.42.0/v3/integrations/nrfiber/nrfiber.go000066400000000000000000000153011510742411500217440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrfiber instruments https://github.com/gofiber/fiber applications. // // Use this package to instrument inbound requests handled by a fiber.App. // Call nrfiber.Middleware to get a fiber.Handler which can be added to your // application as a middleware: // // app := fiber.New() // // Add the nrfiber middleware before other middlewares or routes: // app.Use(nrfiber.Middleware(newrelicApp)) // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrfiber/example/main.go package nrfiber import ( "context" "net/http" "github.com/gofiber/fiber/v2" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpadaptor" ) func init() { internal.TrackUsage("integration", "framework", "fiber", "v1") } // fastHeaderResponseWriter is a lightweight wrapper around Fiber's response // that implements http.ResponseWriter interface type fastHeaderResponseWriter struct { fiberResponse *fiber.Response header http.Header // cached header to avoid repeated conversions statusCode int } func newFastHeaderResponseWriter(resp *fiber.Response) *fastHeaderResponseWriter { return &fastHeaderResponseWriter{ fiberResponse: resp, header: make(http.Header), statusCode: resp.StatusCode(), } } func (w *fastHeaderResponseWriter) Header() http.Header { // Return cached headers to avoid repeated conversions return w.header } func (w *fastHeaderResponseWriter) Write([]byte) (int, error) { // This is a no-op as we don't actually write anything here return 0, nil } func (w *fastHeaderResponseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.fiberResponse.SetStatusCode(statusCode) } // Apply cached headers to the actual Fiber response func (w *fastHeaderResponseWriter) applyHeaders() { for key, values := range w.header { for _, value := range values { w.fiberResponse.Header.Set(key, value) } } } // Transaction returns the Transaction from the context if it exists. func Transaction(ctx context.Context) *newrelic.Transaction { if ctx == nil { return nil } if txn, ok := ctx.Value(internal.TransactionContextKey).(*newrelic.Transaction); ok { return txn } return nil } // FromContext extracts a New Relic transaction from a Fiber context. func FromContext(c *fiber.Ctx) *newrelic.Transaction { return newrelic.FromContext(c.UserContext()) } // getTransactionName returns a transaction name based on the request path. func getTransactionName(c *fiber.Ctx) string { path := c.Path() if path == "" { path = "/" } return string(c.Method()) + " " + path } // fastHTTPToRequest efficiently converts FastHTTP request to http.Request // using fasthttpadaptor which is optimized for this purpose func fastHTTPToRequest(ctx *fasthttp.RequestCtx) *http.Request { req := &http.Request{} fasthttpadaptor.ConvertRequest(ctx, req, true) return req } // Middleware creates a Fiber middleware handler that instruments requests with New Relic. // It starts a New Relic transaction for each request, sets web request and response details, // and handles error tracking. If no New Relic application is configured, it passes the request through. // // router := fiber.New() // // Add the nrfiber middleware before other middlewares or routes: // router.Use(nrfiber.Middleware(app)) func Middleware(app *newrelic.Application) fiber.Handler { return func(c *fiber.Ctx) error { // If no New Relic application is configured, do nothing if app == nil { return c.Next() } // Create New Relic transaction txnName := getTransactionName(c) txn := app.StartTransaction(txnName) defer txn.End() // Store transaction in context for retrieval in handlers ctx := context.WithValue(c.UserContext(), internal.TransactionContextKey, txn) c.SetUserContext(ctx) // Create optimized response writer wrapper w := newFastHeaderResponseWriter(c.Response()) // Set security agent attributes if present if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, string(c.Request().URI().Path())) } // Set web response object txn.SetWebResponse(w) // Use fasthttpadaptor to efficiently convert to http.Request httpReq := fastHTTPToRequest(c.Context()) txn.SetWebRequestHTTP(httpReq) // Execute next handlers err := c.Next() // Apply any headers that were set through the ResponseWriter interface w.applyHeaders() // Report error if any occurred if err != nil { txn.NoticeError(err) } // Update response status code in transaction txn.SetWebResponse(w).WriteHeader(c.Response().StatusCode()) // Send security event if agent is present if newrelic.IsSecurityAgentPresent() { // Convert fiber response headers to http.Header for security event headers := make(http.Header) c.Response().Header.VisitAll(func(key, value []byte) { headers.Add(string(key), string(value)) }) newrelic.GetSecurityAgentInterface().SendEvent( "RESPONSE_HEADER", headers, txn.GetLinkingMetadata().TraceID, ) } return err } } // WrapHandler wraps an existing Fiber handler with New Relic instrumentation // // fiberApp := fiber.New() // // wrappedHandler := WrapHandler(app.Application, "/wrapped", func(c *fiber.Ctx) error { // return c.SendString("Wrapped Handler") // }) // // fiberApp.Get("/wrapped", wrappedHandler) func WrapHandler(app *newrelic.Application, pattern string, handler fiber.Handler) fiber.Handler { if app == nil { return handler } return func(c *fiber.Ctx) error { // Get transaction from context if middleware is already applied if txn := Transaction(c.UserContext()); txn != nil { // Update name if this is a more specific handler txn.SetName(string(c.Method()) + " " + pattern) return handler(c) } // If no transaction exists, create a new one txn := app.StartTransaction(string(c.Method()) + " " + pattern) defer txn.End() // Store in context ctx := context.WithValue(c.UserContext(), internal.TransactionContextKey, txn) c.SetUserContext(ctx) // Create optimized response writer wrapper w := newFastHeaderResponseWriter(c.Response()) // Set web response object txn.SetWebResponse(w) // Use fasthttpadaptor to efficiently convert to http.Request httpReq := fastHTTPToRequest(c.Context()) txn.SetWebRequestHTTP(httpReq) // Call the handler err := handler(c) // Apply any headers that were set through the ResponseWriter interface w.applyHeaders() // Update response status code in transaction txn.SetWebResponse(w).WriteHeader(c.Response().StatusCode()) // Report error if any occurred if err != nil { txn.NoticeError(err) } return err } } go-agent-3.42.0/v3/integrations/nrfiber/nrfiber_context_test.go000066400000000000000000000053741510742411500245600ustar00rootroot00000000000000package nrfiber import ( "context" "errors" "io" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/stretchr/testify/assert" ) // TestMiddleware_ContextPropagation tests that the middleware correctly propagates the transaction context func TestMiddleware_ContextPropagation(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := fiber.New() router.Use(Middleware(app.Application)) router.Get("/txn", func(c *fiber.Ctx) error { txn := FromContext(c) if txn == nil { t.Error("Transaction is nil") } if txn.Name() != "GET /txn" { t.Error("wrong transaction name", txn.Name()) } txn.NoticeError(errors.New("ooops")) c.WriteString("accessTransactionFromContext") return nil }) req, err := http.NewRequest("GET", "/txn", nil) if err != nil { t.Fatal(err) } resp, err := router.Test(req, -1) body, _ := io.ReadAll(resp.Body) if respBody := string(body); respBody != "accessTransactionFromContext" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /txn", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } // Test_Transaction tests that the Transaction function retrieves the // transaction from the context correctly. // It also tests the behavior when the context is nil or does not contain // a transaction. func Test_Transaction(t *testing.T) { // Create a mock transaction mockTxn := &newrelic.Transaction{} // Create a context with the transaction ctx := context.WithValue(context.Background(), internal.TransactionContextKey, mockTxn) // Retrieve the transaction from the context resultTxn := Transaction(ctx) // Verify the transaction was correctly retrieved assert.Equal(t, mockTxn, resultTxn) // Test with nil context assert.Nil(t, Transaction(nil)) // Test with context but no transaction emptyCtx := context.Background() assert.Nil(t, Transaction(emptyCtx)) } // TestNewContext_Transaction tests that nrfiber.Transaction will find a transaction // added to a context using newrelic.NewContext. func TestNewContext_Transaction(t *testing.T) { // This tests that nrfiber.Transaction will find a transaction added to // to a context using newrelic.NewContext. app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) if tx := Transaction(ctx); nil != tx { tx.NoticeError(errors.New("problem")) } txn.End() app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "name", IsWeb: false, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } go-agent-3.42.0/v3/integrations/nrgin/000077500000000000000000000000001510742411500174545ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgin/LICENSE.txt000066400000000000000000000264501510742411500213060ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgin/README.md000066400000000000000000000006701510742411500207360ustar00rootroot00000000000000# v3/integrations/nrgin [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin) Package `nrgin` instruments https://github.com/gin-gonic/gin applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrgin" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgin). go-agent-3.42.0/v3/integrations/nrgin/example/000077500000000000000000000000001510742411500211075ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgin/example/main.go000066400000000000000000000053321510742411500223650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "github.com/gin-gonic/gin" nrgin "github.com/newrelic/go-agent/v3/integrations/nrgin" "github.com/newrelic/go-agent/v3/newrelic" ) func makeGinEndpoint(s string) func(*gin.Context) { return func(c *gin.Context) { c.Writer.WriteString(s) } } func v1login(c *gin.Context) { c.Writer.WriteString("v1 login") } func v1submit(c *gin.Context) { c.Writer.WriteString("v1 submit") } func v1read(c *gin.Context) { c.Writer.WriteString("v1 read") } func endpoint404(c *gin.Context) { c.Writer.WriteHeader(404) c.Writer.WriteString("returning 404") } func endpointChangeCode(c *gin.Context) { // gin.ResponseWriter buffers the response code so that it can be // changed before the first write. c.Writer.WriteHeader(404) c.Writer.WriteHeader(200) c.Writer.WriteString("actually ok!") } func endpointResponseHeaders(c *gin.Context) { // Since gin.ResponseWriter buffers the response code, response headers // can be set afterwards. c.Writer.WriteHeader(200) c.Writer.Header().Set("Content-Type", "application/json") c.Writer.WriteString(`{"zip":"zap"}`) } func endpointNotFound(c *gin.Context) { c.Writer.WriteString("there's no endpoint for that!") } func endpointAccessTransaction(c *gin.Context) { txn := nrgin.Transaction(c) txn.SetName("custom-name") c.Writer.WriteString("changed the name of the transaction!") } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Gin App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigCodeLevelMetricsEnabled(true), ) if nil != err { fmt.Println(err) os.Exit(1) } // Example filter: ignore monitoring for /headers and /anon endpoints ignoreFilter := func(c *gin.Context) bool { if c.Request.URL.Path == "/headers" || c.Request.URL.Path == "/anon" { return false // Do not monitor } return true // Monitor all other requests } router := gin.Default() router.Use(nrgin.Middleware(app, nrgin.WithFilter(ignoreFilter))) router.GET("/404", endpoint404) router.GET("/change", endpointChangeCode) router.GET("/headers", endpointResponseHeaders) router.GET("/txn", endpointAccessTransaction) // Since the handler function name is used as the transaction name, // anonymous functions do not get usefully named. We encourage // transforming anonymous functions into named functions. router.GET("/anon", func(c *gin.Context) { c.Writer.WriteString("anonymous function handler") }) v1 := router.Group("/v1") v1.GET("/login", v1login) v1.GET("/submit", v1submit) v1.GET("/read", v1read) router.NoRoute(endpointNotFound) router.Run(":8000") } go-agent-3.42.0/v3/integrations/nrgin/go.mod000066400000000000000000000004661510742411500205700ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgin // As of Dec 2019, the gin go.mod file uses 1.12: // https://github.com/gin-gonic/gin/blob/master/go.mod go 1.24 require ( github.com/gin-gonic/gin v1.9.1 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgin/nrgin.go000066400000000000000000000154711510742411500211300ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrgin instruments https://github.com/gin-gonic/gin applications. // // Use this package to instrument inbound requests handled by a gin.Engine. // Call nrgin.Middleware to get a gin.HandlerFunc which can be added to your // application as a middleware: // // router := gin.Default() // // Add the nrgin middleware before other middlewares or routes: // router.Use(nrgin.Middleware(app)) // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrgin/example/main.go package nrgin import ( "net/http" "github.com/gin-gonic/gin" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "gin", "v1") } // headerResponseWriter gives the transaction access to response headers and the // response code. type headerResponseWriter struct{ w gin.ResponseWriter } func (w *headerResponseWriter) Header() http.Header { return w.w.Header() } func (w *headerResponseWriter) Write([]byte) (int, error) { return 0, nil } func (w *headerResponseWriter) WriteHeader(int) {} var _ http.ResponseWriter = &headerResponseWriter{} // replacementResponseWriter mimics the behavior of gin.ResponseWriter which // buffers the response code rather than writing it when // gin.ResponseWriter.WriteHeader is called. type replacementResponseWriter struct { gin.ResponseWriter replacement http.ResponseWriter code int written bool } var _ gin.ResponseWriter = &replacementResponseWriter{} func (w *replacementResponseWriter) flushHeader() { if !w.written { w.replacement.WriteHeader(w.code) w.written = true } } func (w *replacementResponseWriter) WriteHeader(code int) { w.code = code w.ResponseWriter.WriteHeader(code) } func (w *replacementResponseWriter) Write(data []byte) (int, error) { w.flushHeader() if newrelic.IsSecurityAgentPresent() { w.replacement.Write(data) } return w.ResponseWriter.Write(data) } func (w *replacementResponseWriter) WriteString(s string) (int, error) { w.flushHeader() if newrelic.IsSecurityAgentPresent() { w.replacement.Write([]byte(s)) } return w.ResponseWriter.WriteString(s) } func (w *replacementResponseWriter) WriteHeaderNow() { w.flushHeader() w.ResponseWriter.WriteHeaderNow() } // Context avoids making this package 1.7+ specific. type Context interface { Value(key interface{}) interface{} } // Transaction returns the transaction stored inside the context, or nil if not // found. func Transaction(c Context) *newrelic.Transaction { if v := c.Value(internal.GinTransactionContextKey); nil != v { if txn, ok := v.(*newrelic.Transaction); ok { return txn } } if v := c.Value(internal.TransactionContextKey); nil != v { if txn, ok := v.(*newrelic.Transaction); ok { return txn } } return nil } type handlerNamer interface { HandlerName() string } func getName(c handlerNamer, useNewNames bool) string { if useNewNames { if fp, ok := c.(interface{ FullPath() string }); ok { return fp.FullPath() } } return c.HandlerName() } // MiddlewareOption defines a functional option for configuring the Middleware. type MiddlewareOption func(*middlewareConfig) type middlewareConfig struct { filterFunc func(*gin.Context) bool } // WithFilter allows users to provide a custom filter function to determine // whether a request should be monitored. func WithFilter(filter func(*gin.Context) bool) MiddlewareOption { return func(cfg *middlewareConfig) { cfg.filterFunc = filter } } // Middleware creates a Gin middleware that instruments requests with optional configurations. // // router := gin.Default() // // Add the nrgin middleware before other middlewares or routes: // router.Use(nrgin.Middleware(app)) // // Gin v1.5.0 introduced the gin.Context.FullPath method which allows for much // improved transaction naming. This Middleware will use that // gin.Context.FullPath if available and fall back to the original // gin.Context.HandlerName if not. If you are using Gin v1.5.0 and wish to // continue using the old transaction names, use // nrgin.MiddlewareHandlerTxnNames. func Middleware(app *newrelic.Application, opts ...MiddlewareOption) gin.HandlerFunc { return middleware(app, true, opts...) } // MiddlewareHandlerTxnNames creates a Gin middleware that instruments // requests. // // router := gin.Default() // // Add the nrgin middleware before other middlewares or routes: // router.Use(nrgin.MiddlewareHandlerTxnNames(app)) // // The use of gin.Context.HandlerName for naming transactions will be removed // in a future release. Available in Gin v1.5.0 and newer is the // gin.Context.FullPath method which allows for much improved transaction // names. Use nrgin.Middleware to take full advantage of this new naming! func MiddlewareHandlerTxnNames(app *newrelic.Application) gin.HandlerFunc { return middleware(app, false) } // WrapRouter extracts API endpoints from the router instance passed to it // which is used to detect application URL mapping(api-endpoints) for provable security. // In this version of the integration, this wrapper is only necessary if you are using the New Relic security agent integration [https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsecurityagent], // but it may be enhanced to provide additional functionality in future releases. // // router := gin.Default() // .... // .... // .... // // nrgin.WrapRouter(router) func WrapRouter(engine *gin.Engine) { if engine != nil && newrelic.IsSecurityAgentPresent() { router := engine.Routes() for _, r := range router { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, internal.HandlerName(r.HandlerFunc)) } } } func middleware(app *newrelic.Application, useNewNames bool, opts ...MiddlewareOption) gin.HandlerFunc { config := &middlewareConfig{} for _, opt := range opts { opt(config) } return func(c *gin.Context) { if config.filterFunc != nil && !config.filterFunc(c) { c.Next() return } traceID := "" if app != nil { name := c.Request.Method + " " + getName(c, useNewNames) w := &headerResponseWriter{w: c.Writer} txn := app.StartTransaction(name, newrelic.WithFunctionLocation(c.Handler())) if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, c.FullPath()) } txn.SetWebRequestHTTP(c.Request) defer txn.End() repl := &replacementResponseWriter{ ResponseWriter: c.Writer, replacement: txn.SetWebResponse(w), code: http.StatusOK, } c.Writer = repl defer repl.flushHeader() c.Set(internal.GinTransactionContextKey, txn) traceID = txn.GetLinkingMetadata().TraceID } c.Next() if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Writer.Header(), traceID) } } } go-agent-3.42.0/v3/integrations/nrgin/nrgin_context_test.go000066400000000000000000000070011510742411500237210ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgin import ( "context" "errors" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func accessTransactionContextContext(c *gin.Context) { var ctx context.Context = c // Transaction is designed to take both a context.Context and a // *gin.Context. txn := Transaction(ctx) txn.NoticeError(errors.New("problem")) c.Writer.WriteString("accessTransactionContextContext") } func TestContextContextTransaction(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/txn", accessTransactionContextContext) txnName := "GET " + pkg + ".accessTransactionContextContext" if useFullPathVersion(gin.Version) { txnName = "GET /txn" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/txn", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "accessTransactionContextContext" { t.Error("wrong response body", respBody) } if response.Code != 200 { t.Error("wrong response code", response.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func accessTransactionFromContext(c *gin.Context) { // This tests that FromContext will find the transaction added to a // *gin.Context and by nrgin.Middleware. txn := newrelic.FromContext(c) txn.NoticeError(errors.New("problem")) c.Writer.WriteString("accessTransactionFromContext") } func TestFromContext(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/txn", accessTransactionFromContext) txnName := "GET " + pkg + ".accessTransactionFromContext" if useFullPathVersion(gin.Version) { txnName = "GET /txn" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/txn", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "accessTransactionFromContext" { t.Error("wrong response body", respBody) } if response.Code != 200 { t.Error("wrong response code", response.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestContextWithoutTransaction(t *testing.T) { txn := Transaction(context.Background()) if txn != nil { t.Error("didn't expect a transaction", txn) } ctx := context.WithValue(context.Background(), internal.TransactionContextKey, 123) txn = Transaction(ctx) if txn != nil { t.Error("didn't expect a transaction", txn) } } func TestNewContextTransaction(t *testing.T) { // This tests that nrgin.Transaction will find a transaction added to // to a context using newrelic.NewContext. app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) if tx := Transaction(ctx); nil != tx { tx.NoticeError(errors.New("problem")) } txn.End() app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "name", IsWeb: false, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } go-agent-3.42.0/v3/integrations/nrgin/nrgin_test.go000066400000000000000000000256201510742411500221640ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgin import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var ( pkg = "github.com/newrelic/go-agent/v3/integrations/nrgin" ) func hello(c *gin.Context) { c.Writer.WriteString("hello response") } func TestBasicRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/hello", hello) txnName := "GET " + pkg + ".hello" if useFullPathVersion(gin.Version) { txnName = "GET /hello" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, UnknownCaller: true, }) } func TestRouterGroup(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) group := router.Group("/group") group.GET("/hello", hello) txnName := "GET " + pkg + ".hello" if useFullPathVersion(gin.Version) { txnName = "GET /group/hello" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/group/hello", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, UnknownCaller: true, }) } func TestAnonymousHandler(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/anon", func(c *gin.Context) { c.Writer.WriteString("anonymous function handler") }) txnName := "GET " + pkg + ".TestAnonymousHandler.func1" if useFullPathVersion(gin.Version) { txnName = "GET /anon" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/anon", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "anonymous function handler" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, UnknownCaller: true, }) } func multipleWriteHeader(c *gin.Context) { // Unlike http.ResponseWriter, gin.ResponseWriter does not immediately // write the first WriteHeader. Instead, it gets buffered until the // first Write call. c.Writer.WriteHeader(200) c.Writer.WriteHeader(500) c.Writer.WriteString("multipleWriteHeader") } func TestMultipleWriteHeader(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/header", multipleWriteHeader) txnName := "GET " + pkg + ".multipleWriteHeader" if useFullPathVersion(gin.Version) { txnName = "GET /header" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/header", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "multipleWriteHeader" { t.Error("wrong response body", respBody) } if response.Code != 500 { t.Error("wrong response code", response.Code) } // Error metrics test the 500 response code capture. app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func accessTransactionGinContext(c *gin.Context) { txn := Transaction(c) txn.NoticeError(errors.New("problem")) c.Writer.WriteString("accessTransactionGinContext") } func TestContextTransaction(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/txn", accessTransactionGinContext) txnName := "GET " + pkg + ".accessTransactionGinContext" if useFullPathVersion(gin.Version) { txnName = "GET /txn" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/txn", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "accessTransactionGinContext" { t.Error("wrong response body", respBody) } if response.Code != 200 { t.Error("wrong response code", response.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestNilApp(t *testing.T) { var app *newrelic.Application router := gin.Default() router.Use(Middleware(app)) router.GET("/hello", hello) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello response" { t.Error("wrong response body", respBody) } } func errorStatus(c *gin.Context) { c.String(500, "an error happened") } // The Gin.Context.Status method behavior changed with this pull // request: https://github.com/gin-gonic/gin/pull/1606. This change // affects our ability to instrument the response code. In Gin v1.4.0 // and below, we always recorded a 200 status, whereas with newer Gin // versions we now correctly capture the status. var statusFixVersion = [...]string{"1", "5"} // Gin added the FullPath method to the Gin.Context in this version. When // available, we use this method to set the transaction name. var fullPathVersion = [...]string{"1", "5"} func useFullPathVersion(v string) bool { return checkVersionIsAtLeast(v, fullPathVersion) } func useStatusFixVersion(v string) bool { return checkVersionIsAtLeast(v, statusFixVersion) } func checkVersionIsAtLeast(checkV string, checkAgainst [2]string) bool { parts := strings.Split(strings.TrimPrefix(checkV, "v"), ".") if len(parts) < 2 { return false } if parts[0] < checkAgainst[0] { return false } return parts[1] >= checkAgainst[1] } func TestStatusCodes(t *testing.T) { // Test that we are correctly able to collect status code. expectCode := 200 if useStatusFixVersion(gin.Version) { expectCode = 500 } app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/err", errorStatus) txnName := "WebTransaction/Go/GET " + pkg + ".errorStatus" if useFullPathVersion(gin.Version) { txnName = "WebTransaction/Go/GET /err" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/err", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "an error happened" { t.Error("wrong response body", respBody) } if response.Code != 500 { t.Error("wrong response code", response.Code) } app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": txnName, "nr.apdexPerfZone": internal.MatchAnything, "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": expectCode, "http.statusCode": expectCode, "request.method": "GET", "request.uri": "/err", "response.headers.contentType": "text/plain; charset=utf-8", "code.function": internal.MatchAnything, "code.namespace": internal.MatchAnything, "code.filepath": internal.MatchAnything, "code.lineno": internal.MatchAnything, }, }}) } func noBody(c *gin.Context) { c.Status(500) } func TestNoResponseBody(t *testing.T) { // Test that when no response body is sent (i.e. c.Writer.Write is never // called) that we still capture status code. expectCode := 200 if useFullPathVersion(gin.Version) { expectCode = 500 } app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/nobody", noBody) txnName := "WebTransaction/Go/GET " + pkg + ".noBody" if useFullPathVersion(gin.Version) { txnName = "WebTransaction/Go/GET /nobody" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/nobody", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "" { t.Error("wrong response body", respBody) } if response.Code != 500 { t.Error("wrong response code", response.Code) } app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": txnName, "nr.apdexPerfZone": internal.MatchAnything, "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": expectCode, "http.statusCode": expectCode, "request.method": "GET", "request.uri": "/nobody", "code.function": internal.MatchAnything, "code.namespace": internal.MatchAnything, "code.filepath": internal.MatchAnything, "code.lineno": internal.MatchAnything, }, }}) } func TestRouteWithParams(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(Middleware(app.Application)) router.GET("/hello/:name/*action", hello) txnName := "GET " + pkg + ".hello" if useFullPathVersion(gin.Version) { // ensure the transaction is named after the route and not the url txnName = "GET /hello/:name/*action" } response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/world/fun", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, UnknownCaller: true, }) } func TestMiddlewareOldNaming(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := gin.Default() router.Use(MiddlewareHandlerTxnNames(app.Application)) router.GET("/hello", hello) txnName := "GET " + pkg + ".hello" response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: txnName, IsWeb: true, UnknownCaller: true, }) } go-agent-3.42.0/v3/integrations/nrgochi/000077500000000000000000000000001510742411500177705ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgochi/LICENSE.txt000066400000000000000000000264501510742411500216220ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgochi/example/000077500000000000000000000000001510742411500214235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgochi/example/main.go000066400000000000000000000040411510742411500226750ustar00rootroot00000000000000// main.go package main import ( "fmt" "net/http" "os" "github.com/go-chi/chi/v5" "github.com/newrelic/go-agent/v3/integrations/nrgochi" "github.com/newrelic/go-agent/v3/newrelic" ) func makeChiEndpoint(s string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(s)) } } func endpoint404(w http.ResponseWriter, r *http.Request) { newrelic.FromContext(r.Context()).NoticeError(fmt.Errorf("returning 404")) w.WriteHeader(404) w.Write([]byte("returning 404")) } func endpointChangeCode(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) w.WriteHeader(200) w.Write([]byte("actually ok!")) } func endpointResponseHeaders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"zip":"zap"}`)) } func endpointNotFound(w http.ResponseWriter, r *http.Request) { w.Write([]byte("there's no endpoint for that!")) } func endpointAccessTransaction(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) txn.SetName("custom-name") w.Write([]byte("changed the name of the transaction!")) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Chi App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigCodeLevelMetricsEnabled(true), ) if err != nil { fmt.Println(err) os.Exit(1) } router := chi.NewRouter() router.Use(nrgochi.Middleware(app)) router.Get("/404", endpoint404) router.Get("/change", endpointChangeCode) router.Get("/headers", endpointResponseHeaders) router.Get("/txn", endpointAccessTransaction) // Since the handler function name is used as the transaction name, // anonymous functions do not get usefully named. We encourage // transforming anonymous functions into named functions. router.Get("/anon", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("anonymous function handler")) }) router.NotFound(endpointNotFound) http.ListenAndServe(":8000", router) } go-agent-3.42.0/v3/integrations/nrgochi/go.mod000066400000000000000000000003161510742411500210760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgochi go 1.24 require ( github.com/go-chi/chi/v5 v5.2.2 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgochi/nrgochi.go000066400000000000000000000065141510742411500217560ustar00rootroot00000000000000// Package nrgochi instruments https://github.com/go-chi/chi applications. // // Use this package to instrument inbound requests handled by a chi.Router. // Call nrgochi.Middleware to get a chi.Middleware which can be added to your // application as a middleware: // // router := chi.NewRouter() // // Add the nrgochi middleware before other middlewares or routes: // router.Use(nrgochi.Middleware(app)) // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrgochi/example/main.go package nrgochi import ( "net/http" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "gochi", "v1") } // headerResponseWriter gives the transaction access to response headers and the // response code. type headerResponseWriter struct{ w http.ResponseWriter } func (w *headerResponseWriter) Header() http.Header { return w.w.Header() } func (w *headerResponseWriter) Write([]byte) (int, error) { return 0, nil } func (w *headerResponseWriter) WriteHeader(int) {} var _ http.ResponseWriter = &headerResponseWriter{} // replacementResponseWriter mimics the behavior of http.ResponseWriter which // buffers the response code rather than writing it when // http.ResponseWriter.WriteHeader is called. type replacementResponseWriter struct { http.ResponseWriter replacement http.ResponseWriter code int written bool } var _ http.ResponseWriter = &replacementResponseWriter{} func (w *replacementResponseWriter) flushHeader() { if !w.written { w.replacement.WriteHeader(w.code) w.written = true } } func (w *replacementResponseWriter) WriteHeader(code int) { w.code = code w.ResponseWriter.WriteHeader(code) } func (w *replacementResponseWriter) Write(data []byte) (int, error) { w.flushHeader() if newrelic.IsSecurityAgentPresent() { w.replacement.Write(data) } return w.ResponseWriter.Write(data) } func (w *replacementResponseWriter) WriteString(s string) (int, error) { w.flushHeader() if newrelic.IsSecurityAgentPresent() { w.replacement.Write([]byte(s)) } return w.ResponseWriter.Write([]byte(s)) } // Middleware creates a Chi middleware that instruments requests. // // router := chi.NewRouter() // // Add the nrgochi middleware before other middlewares or routes: // router.Use(nrgochi.Middleware(app)) func Middleware(app *newrelic.Application) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { traceID := "" if app != nil { name := r.Method + " " + r.URL.Path hdrWriter := &headerResponseWriter{w: w} txn := app.StartTransaction(name) if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, r.URL.Path) } txn.SetWebRequestHTTP(r) defer txn.End() repl := &replacementResponseWriter{ ResponseWriter: w, replacement: txn.SetWebResponse(hdrWriter), code: http.StatusOK, } w = repl defer repl.flushHeader() ctx := newrelic.NewContext(r.Context(), txn) r = r.WithContext(ctx) traceID = txn.GetLinkingMetadata().TraceID } next.ServeHTTP(w, r) if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header(), traceID) } }) } } go-agent-3.42.0/v3/integrations/nrgochi/nrgochi_test.go000066400000000000000000000041511510742411500230100ustar00rootroot00000000000000package nrgochi import ( "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) } func TestBasicRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := chi.NewRouter() router.Use(Middleware(app.Application)) router.Get("/hello", hello) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello", IsWeb: true, UnknownCaller: true, }) } func TestAnonymousFunctions(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := chi.NewRouter() router.Use(Middleware(app.Application)) router.Get("/helloAnon", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello anon")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/helloAnon", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hello anon" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /helloAnon", IsWeb: true, UnknownCaller: true, }) } func TestWriteHeader(t *testing.T) { app := integrationsupport.NewBasicTestApp() router := chi.NewRouter() router.Use(Middleware(app.Application)) router.Get("/writeheader", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/writeheader", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if response.Code != 404 { t.Error("wrong response code", response.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /writeheader", IsWeb: true, UnknownCaller: true, }) } go-agent-3.42.0/v3/integrations/nrgorilla/000077500000000000000000000000001510742411500203305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgorilla/LICENSE.txt000066400000000000000000000264501510742411500221620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgorilla/README.md000066400000000000000000000007161510742411500216130ustar00rootroot00000000000000# v3/integrations/nrgorilla [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla) Package `nrgorilla` instruments https://github.com/gorilla/mux applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrgorilla" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla). go-agent-3.42.0/v3/integrations/nrgorilla/example/000077500000000000000000000000001510742411500217635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgorilla/example/main.go000066400000000000000000000026701510742411500232430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "net/http" "os" "github.com/gorilla/mux" "github.com/newrelic/go-agent/v3/integrations/nrgorilla" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func makeHandler(text string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(text)) }) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Gorilla App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } r := mux.NewRouter() r.Use(nrgorilla.Middleware(app)) r.Handle("/", makeHandler("index")) r.Handle("/alpha", makeHandler("alpha")) users := r.PathPrefix("/users").Subrouter() users.Handle("/add", makeHandler("adding user")) users.Handle("/delete", makeHandler("deleting user")) // The route name will be used as the transaction name if one is set. r.Handle("/named", makeHandler("named route")).Name("special-name-route") // The NotFoundHandler and MethodNotAllowedHandler must be instrumented // separately. _, r.NotFoundHandler = newrelic.WrapHandle(app, "NotFoundHandler", makeHandler("not found")) _, r.MethodNotAllowedHandler = newrelic.WrapHandle(app, "MethodNotAllowedHandler", makeHandler("method not allowed")) http.ListenAndServe(":8000", r) } go-agent-3.42.0/v3/integrations/nrgorilla/example_test.go000066400000000000000000000026111510742411500233510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgorilla_test import ( "net/http" "github.com/gorilla/mux" "github.com/newrelic/go-agent/v3/integrations/nrgorilla" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) var ( app *newrelic.Application MyCustomMiddleware mux.MiddlewareFunc ) func makeHandler(text string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(text)) }) } func ExampleMiddleware() { r := mux.NewRouter() r.Use(nrgorilla.Middleware(app)) // All handlers and custom middlewares will be instrumented. The // transaction will be available in the Request's context. r.Use(MyCustomMiddleware) r.Handle("/", makeHandler("index")) http.ListenAndServe(":8000", r) } func ExampleMiddleware_specialHandlers() { r := mux.NewRouter() r.Use(nrgorilla.Middleware(app)) // The NotFoundHandler and MethodNotAllowedHandler must be instrumented // separately using newrelic.WrapHandle. The second argument to // newrelic.WrapHandle is used as the transaction name; the string returned // from newrelic.WrapHandle should be ignored. _, r.NotFoundHandler = newrelic.WrapHandle(app, "NotFoundHandler", makeHandler("not found")) _, r.MethodNotAllowedHandler = newrelic.WrapHandle(app, "MethodNotAllowedHandler", makeHandler("method not allowed")) } go-agent-3.42.0/v3/integrations/nrgorilla/go.mod000066400000000000000000000005731510742411500214430ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgorilla // As of Dec 2019, the gorilla/mux go.mod file uses 1.12: // https://github.com/gorilla/mux/blob/master/go.mod go 1.24 require ( // v1.7.0 is the earliest version of Gorilla using modules. github.com/gorilla/mux v1.7.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgorilla/nrgorilla.go000066400000000000000000000123161510742411500226530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrgorilla instruments https://github.com/gorilla/mux applications. // // Use this package to instrument inbound requests handled by a gorilla // mux.Router. Use the nrgorilla.Middleware as the first middleware registered // with your router. // // Complete example: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrgorilla/example/main.go package nrgorilla import ( "net/http" "github.com/gorilla/mux" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "gorilla", "v1") } type instrumentedHandler struct { app *newrelic.Application orig http.Handler } func (h instrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if newrelic.FromContext(r.Context()) == nil { name := routeName(r) txn := h.app.StartTransaction(name) txn.SetWebRequestHTTP(r) w = txn.SetWebResponse(w) defer txn.End() r = newrelic.RequestWithTransactionContext(r, txn) } h.orig.ServeHTTP(w, r) } func instrumentRoute(h http.Handler, app *newrelic.Application) http.Handler { if _, ok := h.(instrumentedHandler); ok { return h } return instrumentedHandler{ orig: h, app: app, } } func routeName(r *http.Request) string { route := mux.CurrentRoute(r) if nil == route { return "NotFoundHandler" } if n := route.GetName(); n != "" { return n } if n, _ := route.GetPathTemplate(); n != "" { return r.Method + " " + n } n, _ := route.GetHostTemplate() return r.Method + " " + n } func handlerName(r *http.Request) string { route := mux.CurrentRoute(r) if nil == route { return r.RequestURI } if n, _ := route.GetPathTemplate(); n != "" { return n } else { return r.RequestURI } } // InstrumentRoutes instruments requests through the provided mux.Router. Use // this after the routes have been added to the router. // // Deprecated: Use the newer and more complete Middleware method instead. func InstrumentRoutes(r *mux.Router, app *newrelic.Application) *mux.Router { if app != nil { r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { h := instrumentRoute(route.GetHandler(), app) route.Handler(h) return nil }) if nil != r.NotFoundHandler { r.NotFoundHandler = instrumentRoute(r.NotFoundHandler, app) } } return r } // Middleware creates a new mux.MiddlewareFunc. When used, this middleware // will create a transaction for each inbound request. The transaction will be // available in the Request's context throughout the call chain, including in // any other middlewares that are registered after this one. For this reason, // it is important for this middleware to be registered first. // // Note that mux.MiddlewareFuncs are not called for the NotFoundHandler or // MethodNotAllowedHandler. To instrument these handlers, use // newrelic.WrapHandle // (https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandle). // // Note that if you are moving from the now deprecated InstrumentRoutes to this // Middleware, the reported time of your transactions may increase. This is // expected and nothing to worry about. This method includes in the // transaction total time request time that is spent in other custom // middlewares whereas InstrumentRoutes does not. func Middleware(app *newrelic.Application) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := routeName(r) txn := app.StartTransaction(name) defer txn.End() if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, handlerName(r)) } txn.SetWebRequestHTTP(r) w = txn.SetWebResponse(w) r = newrelic.RequestWithTransactionContext(r, txn) next.ServeHTTP(w, r) if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header(), txn.GetLinkingMetadata().TraceID) } }) } } // WrapRouter extracts API endpoints from the router object passed to it // which is used to detect application URL mapping(api-endpoints) for provable security. // In this version of the integration, this wrapper is only necessary if you are using the New Relic security agent integration [https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsecurityagent], // but it may be enhanced to provide additional functionality in future releases. // // r := mux.NewRouter() // .... // .... // .... // // nrgorilla.WrapRouter(router) func WrapRouter(router *mux.Router) { if router != nil && newrelic.IsSecurityAgentPresent() { router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { path, err1 := route.GetPathTemplate() if err1 != nil { return nil } methods, _ := route.GetMethods() if len(methods) == 0 { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", path, "*", internal.HandlerName(route.GetHandler())) } else { for _, method := range methods { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", path, method, internal.HandlerName(route.GetHandler())) } } return nil }) } } go-agent-3.42.0/v3/integrations/nrgorilla/nrgorilla_test.go000066400000000000000000000166651510742411500237250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgorilla import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/gorilla/mux" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func makeHandler(text string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(text)) }) } func TestBasicRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.Handle("/alpha", makeHandler("alpha response")) InstrumentRoutes(r, app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/alpha", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "alpha response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /alpha", IsWeb: true, UnknownCaller: true, }) } func TestSubrouterRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() users := r.PathPrefix("/users").Subrouter() users.Handle("/add", makeHandler("adding user")) InstrumentRoutes(r, app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/users/add", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "adding user" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /users/add", IsWeb: true, UnknownCaller: true, }) } func TestNamedRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.Handle("/named", makeHandler("named route")).Name("special-name-route") InstrumentRoutes(r, app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/named", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "named route" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "special-name-route", IsWeb: true, UnknownCaller: true, }) } func TestRouteNotFound(t *testing.T) { app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.Write([]byte("not found")) }) // Tests that routes do not get double instrumented when // InstrumentRoutes is called twice by expecting error metrics with a // count of 1. InstrumentRoutes(r, app.Application) InstrumentRoutes(r, app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/foo", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "not found" { t.Error("wrong response body", respBody) } if response.Code != 500 { t.Error("wrong response code", response.Code) } // Error metrics test the 500 response code capture. app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "NotFoundHandler", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestNilApp(t *testing.T) { var app *newrelic.Application r := mux.NewRouter() r.Handle("/alpha", makeHandler("alpha response")) InstrumentRoutes(r, app) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/alpha", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "alpha response" { t.Error("wrong response body", respBody) } } func TestMiddlewareBasicRoute(t *testing.T) { app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.Handle("/alpha", makeHandler("alpha response")) r.Use(Middleware(app.Application)) r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // ensure that the txn is added to the context and accessible by // middlewares newrelic.FromContext(r.Context()).NoticeError(errors.New("oops")) next.ServeHTTP(w, r) }) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/alpha", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "alpha response" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /alpha", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestMiddlewareNilApp(t *testing.T) { r := mux.NewRouter() r.Handle("/alpha", makeHandler("alpha response")) r.Use(Middleware(nil)) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/alpha", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "alpha response" { t.Error("wrong response body", respBody) } } func TestMiddlewareAndInstrumentRoutes(t *testing.T) { // Test that only one transaction is created when Middleware and // InstrumentRoutes are used. app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.Handle("/alpha", makeHandler("alpha response")) r.Use(Middleware(app.Application)) InstrumentRoutes(r, app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/alpha", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "alpha response" { t.Error("wrong response body", respBody) } app.ExpectTxnEvents(t, []internal.WantEvent{ {}, }) } func TestMiddlewareNotFoundHandler(t *testing.T) { // This test will fail if gorilla ever decides to run the NotFoundHandler // through the middleware. app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) w.Write([]byte("not found")) }) r.Use(Middleware(app.Application)) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/foo", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "not found" { t.Error("wrong response body", respBody) } if response.Code != 404 { t.Error("wrong response code", response.Code) } // make sure no txn events were created app.ExpectTxnEvents(t, []internal.WantEvent{}) } func TestMiddlewareMethodNotAllowedHandler(t *testing.T) { // This test will fail if gorilla ever decides to run the // MethodNotAllowedHandler through the middleware. app := integrationsupport.NewBasicTestApp() r := mux.NewRouter() r.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(405) w.Write([]byte("method not allowed")) }) r.Use(Middleware(app.Application)) r.Handle("/foo", makeHandler("index")).Methods("POST") response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/foo", nil) if err != nil { t.Fatal(err) } r.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "method not allowed" { t.Error("wrong response body", respBody) } if response.Code != 405 { t.Error("wrong response code", response.Code) } // make sure no txn events were created app.ExpectTxnEvents(t, []internal.WantEvent{}) } go-agent-3.42.0/v3/integrations/nrgraphgophers/000077500000000000000000000000001510742411500213705ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgraphgophers/LICENSE.txt000066400000000000000000000264501510742411500232220ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgraphgophers/README.md000066400000000000000000000007711510742411500226540ustar00rootroot00000000000000# v3/integrations/nrgraphgophers [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers) Package `nrgraphgophers` instruments https://github.com/graph-gophers/graphql-go applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrgraphgophers" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphgophers). go-agent-3.42.0/v3/integrations/nrgraphgophers/example/000077500000000000000000000000001510742411500230235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgraphgophers/example/main.go000066400000000000000000000025121510742411500242760ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "log" "net/http" "os" graphql "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" "github.com/newrelic/go-agent/v3/integrations/nrgraphgophers" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) type query struct{} func (*query) Hello() string { return "Hello, world!" } func main() { // First create your New Relic Application: app, err := newrelic.NewApplication( newrelic.ConfigAppName("GraphQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } s := `type Query { hello: String! }` // Then add a graphql.Tracer(nrgraphgophers.NewTracer()) option to your // schema parsing to get field and query segment instrumentation: opt := graphql.Tracer(nrgraphgophers.NewTracer()) schema := graphql.MustParseSchema(s, &query{}, opt) // Finally, instrument your request handler using newrelic.WrapHandle // to create transactions for requests: http.Handle(newrelic.WrapHandle(app, "/graphql", &relay.Handler{Schema: schema})) // To test, run: // curl -X POST -d '{"query": "query HelloOperation { hello }" }' localhost:8000/graphql log.Fatal(http.ListenAndServe(":8000", nil)) } go-agent-3.42.0/v3/integrations/nrgraphgophers/go.mod000066400000000000000000000006221510742411500224760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgraphgophers // As of Jan 2020, the graphql-go go.mod file uses 1.13: // https://github.com/graph-gophers/graphql-go/blob/master/go.mod go 1.24 require ( // graphql-go has no tagged releases as of Jan 2020. github.com/graph-gophers/graphql-go v1.3.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgraphgophers/nrgraphgophers.go000066400000000000000000000064141510742411500247550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrgraphgophers instruments https://github.com/graph-gophers/graphql-go // applications. // // This package creates a graphql-go Tracer that adds adds segment // instrumentation to your graphql request transactions. package nrgraphgophers import ( "context" "sync" "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/introspection" "github.com/graph-gophers/graphql-go/trace" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "graph-gophers") } type requestIDContextKeyType struct{} var ( requestIDContextKey requestIDContextKeyType = struct{}{} ) type requestID uint64 type tracer struct { sync.Mutex counter requestID activeFields map[requestID]int } // NewTracer creates a new trace.Tracer that adds segment instrumentation // to the transaction. func NewTracer() trace.Tracer { return &tracer{ activeFields: make(map[requestID]int), } } func (t *tracer) newRequestID() requestID { t.Lock() defer t.Unlock() id := t.counter t.counter++ return id } func (t *tracer) removeFields(id requestID) { t.Lock() defer t.Unlock() delete(t.activeFields, id) } func (t *tracer) startField(id requestID) (async bool) { t.Lock() defer t.Unlock() numActive := t.activeFields[id] t.activeFields[id] = numActive + 1 return numActive > 0 } func (t *tracer) stopField(id requestID) { t.Lock() defer t.Unlock() t.activeFields[id] = t.activeFields[id] - 1 } func (t *tracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, trace.TraceQueryFinishFunc) { txn := newrelic.FromContext(ctx) if nil == txn { return ctx, func([]*errors.QueryError) {} } // Since this https://github.com/graph-gophers/graphql-go/pull/374 was // merged in Feb 2020, an empty operation name should be impossible. // This conditional is left here in case someone is using an early // graphql-go version. if operationName == "" { operationName = "unknown operation" } segment := txn.StartSegment(operationName) id := t.newRequestID() ctx = context.WithValue(ctx, requestIDContextKey, id) return ctx, func(errs []*errors.QueryError) { t.removeFields(id) for _, err := range errs { txn.NoticeError(err) } segment.End() } } func (t *tracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, trace.TraceFieldFinishFunc) { txn := newrelic.FromContext(ctx) if nil == txn { return ctx, func(*errors.QueryError) {} } id, ok := ctx.Value(requestIDContextKey).(requestID) if !ok { return ctx, func(*errors.QueryError) {} } async := t.startField(id) if async { txn = txn.NewGoroutine() // Update the context with the async transaction in case it is // possible to make segments inside the field handling code. ctx = newrelic.NewContext(ctx, txn) } segment := txn.StartSegment(fieldName) return ctx, func(*errors.QueryError) { // Notice errors in query finish function to avoid double // noticing errors. t.stopField(id) segment.End() } } go-agent-3.42.0/v3/integrations/nrgraphgophers/nrgraphgophers_example_test.go000066400000000000000000000023611510742411500275240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgraphgophers_test import ( "log" "net/http" "os" graphql "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" "github.com/newrelic/go-agent/v3/integrations/nrgraphgophers" "github.com/newrelic/go-agent/v3/newrelic" ) type query struct{} func (*query) Hello() string { return "hello world" } func Example() { // First create your New Relic Application: app, err := newrelic.NewApplication( newrelic.ConfigAppName("GraphQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } querySchema := `type Query { hello: String! }` // Then add a graphql.Tracer(nrgraphgophers.NewTracer()) option to your // schema parsing to get field and query segment instrumentation: opt := graphql.Tracer(nrgraphgophers.NewTracer()) schema := graphql.MustParseSchema(querySchema, &query{}, opt) // Finally, instrument your request handler using newrelic.WrapHandle // to create transactions for requests: http.Handle(newrelic.WrapHandle(app, "/", &relay.Handler{Schema: schema})) log.Fatal(http.ListenAndServe(":8000", nil)) } go-agent-3.42.0/v3/integrations/nrgraphgophers/nrgraphgophers_test.go000066400000000000000000000222471510742411500260160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgraphgophers import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" graphql "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/introspection" "github.com/graph-gophers/graphql-go/relay" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestFieldManagementSync(t *testing.T) { tracer := NewTracer().(*tracer) id1 := tracer.newRequestID() id2 := tracer.newRequestID() if id1 == id2 { t.Fatal(id1, id2) } if async := tracer.startField(id1); async { t.Fatal(async) } if async := tracer.startField(id2); async { t.Fatal(async) } tracer.stopField(id1) if async := tracer.startField(id1); async { t.Fatal(async) } tracer.stopField(id2) tracer.stopField(id1) if tracer.activeFields[id1] != 0 || tracer.activeFields[id2] != 0 { t.Fatal(tracer.activeFields) } tracer.removeFields(id2) tracer.removeFields(id1) if len(tracer.activeFields) != 0 { t.Fatal(tracer.activeFields) } } func TestFieldManagementAsync(t *testing.T) { tracer := NewTracer().(*tracer) id1 := tracer.newRequestID() id2 := tracer.newRequestID() if id1 == id2 { t.Fatal(id1, id2) } if async := tracer.startField(id1); async { t.Fatal(async) } if async := tracer.startField(id1); !async { t.Fatal(async) } if async := tracer.startField(id2); async { t.Fatal(async) } tracer.stopField(id1) if async := tracer.startField(id1); !async { t.Fatal(async) } tracer.stopField(id2) tracer.stopField(id1) tracer.stopField(id1) if tracer.activeFields[id1] != 0 || tracer.activeFields[id2] != 0 { t.Fatal(tracer.activeFields) } tracer.removeFields(id2) tracer.removeFields(id1) if len(tracer.activeFields) != 0 { t.Fatal(tracer.activeFields) } } func TestQueryWithAsyncFields(t *testing.T) { app := integrationsupport.NewBasicTestApp() tracer := NewTracer() txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(nil) ctx := newrelic.NewContext(context.Background(), txn) ctx, queryFinish := tracer.TraceQuery(ctx, "queryString", "MyOperation", map[string]interface{}{}, map[string]*introspection.Type{}) _, fieldFinish1 := tracer.TraceField(ctx, "label", "typeName", "field1", true, map[string]interface{}{}) _, fieldFinish2 := tracer.TraceField(ctx, "label", "typeName", "field2", true, map[string]interface{}{}) fieldFinish1(nil) fieldFinish2(nil) queryFinish(nil) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction"}, {Name: "WebTransaction/Go/hello"}, {Name: "WebTransactionTotalTime"}, {Name: "WebTransactionTotalTime/Go/hello"}, {Name: "Apdex"}, {Name: "Apdex/Go/hello"}, {Name: "HttpDispatcher"}, {Name: "Custom/MyOperation"}, {Name: "Custom/MyOperation", Scope: "WebTransaction/Go/hello"}, {Name: "Custom/field2"}, {Name: "Custom/field2", Scope: "WebTransaction/Go/hello"}, {Name: "Custom/field1"}, {Name: "Custom/field1", Scope: "WebTransaction/Go/hello"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Forced: nil}, }) } type query struct{} func (*query) Hello() string { return "hello world" } func (*query) Problem() (string, error) { return "", errors.New("something went wrong") } func (*query) Zip() string { return "zip" } func (*query) Zap() string { return "zap" } const ( querySchema = `type Query { hello: String! problem: String! zip: String! zap: String! }` ) func TestQueryRequest(t *testing.T) { app := integrationsupport.NewBasicTestApp() opt := graphql.Tracer(NewTracer()) schema := graphql.MustParseSchema(querySchema, &query{}, opt) handler := &relay.Handler{Schema: schema} mux := http.NewServeMux() mux.Handle(newrelic.WrapHandle(app.Application, "/", handler)) body := `{ "query": "query HelloOperation { hello }", "operationName": "HelloOperation" }` req, err := http.NewRequest("POST", "/", strings.NewReader(body)) if err != nil { t.Fatal(err) } rw := httptest.NewRecorder() mux.ServeHTTP(rw, req) if b := rw.Body.String(); b != `{"data":{"hello":"hello world"}}` { t.Error(b) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction"}, {Name: "WebTransaction/Go/POST /"}, {Name: "WebTransactionTotalTime"}, {Name: "WebTransactionTotalTime/Go/POST /"}, {Name: "Apdex"}, {Name: "Apdex/Go/POST /"}, {Name: "HttpDispatcher"}, {Name: "Custom/HelloOperation"}, {Name: "Custom/HelloOperation", Scope: "WebTransaction/Go/POST /"}, {Name: "Custom/hello"}, {Name: "Custom/hello", Scope: "WebTransaction/Go/POST /"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Forced: nil}, }) } func TestQueryRequestUnknownOperation(t *testing.T) { // Test the situation where "operationName" is not provided in the // request body. app := integrationsupport.NewBasicTestApp() opt := graphql.Tracer(NewTracer()) schema := graphql.MustParseSchema(querySchema, &query{}, opt) handler := &relay.Handler{Schema: schema} mux := http.NewServeMux() mux.Handle(newrelic.WrapHandle(app.Application, "/", handler)) body := `{ "query": "query HelloOperation { hello }" }` req, err := http.NewRequest("POST", "/", strings.NewReader(body)) if err != nil { t.Fatal(err) } rw := httptest.NewRecorder() mux.ServeHTTP(rw, req) if b := rw.Body.String(); b != `{"data":{"hello":"hello world"}}` { t.Error(b) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction"}, {Name: "WebTransaction/Go/POST /"}, {Name: "WebTransactionTotalTime"}, {Name: "WebTransactionTotalTime/Go/POST /"}, {Name: "Apdex"}, {Name: "Apdex/Go/POST /"}, {Name: "HttpDispatcher"}, {Name: "Custom/HelloOperation"}, {Name: "Custom/HelloOperation", Scope: "WebTransaction/Go/POST /"}, {Name: "Custom/hello"}, {Name: "Custom/hello", Scope: "WebTransaction/Go/POST /"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Forced: nil}, }) } func TestQueryRequestError(t *testing.T) { app := integrationsupport.NewBasicTestApp() opt := graphql.Tracer(NewTracer()) schema := graphql.MustParseSchema(querySchema, &query{}, opt) handler := &relay.Handler{Schema: schema} mux := http.NewServeMux() mux.Handle(newrelic.WrapHandle(app.Application, "/", handler)) body := `{ "query": "query ProblemOperation { problem }", "operationName": "ProblemOperation" }` req, err := http.NewRequest("POST", "/", strings.NewReader(body)) if err != nil { t.Fatal(err) } rw := httptest.NewRecorder() mux.ServeHTTP(rw, req) if b := rw.Body.String(); b != `{"errors":[{"message":"something went wrong","path":["problem"]}],"data":null}` { t.Error(b) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction"}, {Name: "WebTransaction/Go/POST /"}, {Name: "WebTransactionTotalTime"}, {Name: "WebTransactionTotalTime/Go/POST /"}, {Name: "Apdex"}, {Name: "Apdex/Go/POST /"}, {Name: "HttpDispatcher"}, {Name: "Custom/ProblemOperation"}, {Name: "Custom/ProblemOperation", Scope: "WebTransaction/Go/POST /"}, {Name: "Custom/problem"}, {Name: "Custom/problem", Scope: "WebTransaction/Go/POST /"}, {Name: "Errors/all"}, {Name: "Errors/allWeb"}, {Name: "Errors/WebTransaction/Go/POST /"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Forced: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb"}, }) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/POST /", Msg: "graphql: something went wrong", }}) } func TestQueryRequestMultipleFields(t *testing.T) { app := integrationsupport.NewBasicTestApp() opt := graphql.Tracer(NewTracer()) schema := graphql.MustParseSchema(querySchema, &query{}, opt) handler := &relay.Handler{Schema: schema} mux := http.NewServeMux() mux.Handle(newrelic.WrapHandle(app.Application, "/", handler)) body := `{ "query": "query Multiple { zip zap }", "operationName": "Multiple" }` req, err := http.NewRequest("POST", "/", strings.NewReader(body)) if err != nil { t.Fatal(err) } rw := httptest.NewRecorder() mux.ServeHTTP(rw, req) if b := rw.Body.String(); b != `{"data":{"zip":"zip","zap":"zap"}}` { t.Error(b) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction"}, {Name: "WebTransaction/Go/POST /"}, {Name: "WebTransactionTotalTime"}, {Name: "WebTransactionTotalTime/Go/POST /"}, {Name: "Apdex"}, {Name: "Apdex/Go/POST /"}, {Name: "HttpDispatcher"}, {Name: "Custom/Multiple"}, {Name: "Custom/Multiple", Scope: "WebTransaction/Go/POST /"}, {Name: "Custom/zip"}, {Name: "Custom/zip", Scope: "WebTransaction/Go/POST /"}, {Name: "Custom/zap"}, {Name: "Custom/zap", Scope: "WebTransaction/Go/POST /"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Forced: nil}, }) } go-agent-3.42.0/v3/integrations/nrgraphqlgo/000077500000000000000000000000001510742411500206635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgraphqlgo/LICENSE.txt000066400000000000000000000264501510742411500225150ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgraphqlgo/README.md000066400000000000000000000012301510742411500221360ustar00rootroot00000000000000# v3/integrations/nrgraphqlgo [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo) Package `nrgraphql` instruments https://github.com/graphql-go/graphql applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo" ``` Note that New Relic has support for more than one GraphQL framework, and both packages are named `nrgraphql`, so please ensure you are using the integration for the correct framework. For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo). go-agent-3.42.0/v3/integrations/nrgraphqlgo/example/000077500000000000000000000000001510742411500223165ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgraphqlgo/example/LICENSE.txt000066400000000000000000000264501510742411500241500ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgraphqlgo/example/go.mod000066400000000000000000000006351510742411500234300ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo/example go 1.24 require ( github.com/graphql-go/graphql v0.8.1 github.com/graphql-go/graphql-go-handler v0.2.3 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo v1.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo => ../ replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/nrgraphqlgo/example/main.go000066400000000000000000000042001510742411500235650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "net/http" "os" "time" "github.com/graphql-go/graphql" handler "github.com/graphql-go/graphql-go-handler" "github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo" "github.com/newrelic/go-agent/v3/newrelic" ) var schema = func() graphql.Schema { schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "latestPost": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { time.Sleep(time.Second) return "Hello World!", nil }, }, "randomNumber": &graphql.Field{ Type: graphql.Int, Resolve: func(p graphql.ResolveParams) (interface{}, error) { time.Sleep(time.Second) return 5, nil }, }, "erroring": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { time.Sleep(time.Second) return nil, errors.New("oooops") }, }, }, }), // 1. Add the nrgraphqlgo.Extension to the schema Extensions: []graphql.Extension{nrgraphqlgo.Extension{}}, }) if err != nil { panic(err) } return schema }() func main() { // 2. Create the New Relic application app, err := newrelic.NewApplication( newrelic.ConfigAppName("Example GraphQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { fmt.Println(err) os.Exit(1) } h := handler.New(&handler.Config{ Schema: &schema, Pretty: true, GraphiQL: true, }) // 3. Make sure to instrument your HTTP handler, which will // create/end transactions, record error codes, and add // the transactions to the context. http.Handle(newrelic.WrapHandle(app, "/graphql", h)) // You can test your example query with curl: // curl -X POST \ // -H "Content-Type: application/json" \ // -d '{"query": "{latestPost, randomNumber}"}' \ // localhost:8080/graphql http.ListenAndServe(":8080", nil) } go-agent-3.42.0/v3/integrations/nrgraphqlgo/go.mod000066400000000000000000000003301510742411500217650ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo go 1.24 require ( github.com/graphql-go/graphql v0.8.1 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgraphqlgo/nrgraphqlgo.go000066400000000000000000000106451510742411500235440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrgraphqlgo instruments https://github.com/graphql-go/graphql // applications. // // This package creates an Extension that adds segment // instrumentation for each portion of the GraphQL execution // (Parse, Validation, Execution, ResolveField) to your GraphQL // request transactions. Errors in any of these steps will // be noticed using NoticeError // (https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.NoticeError) // // Please note that you must also instrument your web request handlers // and put the transaction into the context object in order to // utilize this instrumentation. For example, you could use // newrelic.WrapHandle (https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandle) // or newrelic.WrapHandleFunc (https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#WrapHandleFunc) // or you could use a New Relic integration for the web framework you are using // if it is available (for example, // https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgorilla) // // For a complete example, including instrumenting a graphql-go-handler, see: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrgraphqlgo/example/main.go package nrgraphqlgo import ( "context" "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/gqlerrors" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "graphql-go") } // Extension is an extension that creates segments for New Relic, tracking each // step of the execution process type Extension struct{} var _ graphql.Extension = Extension{} // Init is used to help you initialize the extension - in this case, a noop func (Extension) Init(ctx context.Context, params *graphql.Params) context.Context { if params != nil && newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("GRAPHQL", params.RequestString != "", len(params.VariableValues) != 0) } return ctx } // Name returns the name of the extension func (Extension) Name() string { return "New Relic Extension" } // ParseDidStart is called before parsing starts func (Extension) ParseDidStart(ctx context.Context) (context.Context, graphql.ParseFinishFunc) { txn := newrelic.FromContext(ctx) seg := txn.StartSegment("Parse") return ctx, func(err error) { if err != nil { txn.NoticeError(err) } seg.End() } } // ValidationDidStart is called before the validation begins func (Extension) ValidationDidStart(ctx context.Context) (context.Context, graphql.ValidationFinishFunc) { txn := newrelic.FromContext(ctx) seg := txn.StartSegment("Validation") return ctx, func(errs []gqlerrors.FormattedError) { for _, err := range errs { txn.NoticeError(err) } seg.End() } } // ExecutionDidStart is called before the execution begins func (Extension) ExecutionDidStart(ctx context.Context) (context.Context, graphql.ExecutionFinishFunc) { txn := newrelic.FromContext(ctx) seg := txn.StartSegment("Execution") if newrelic.IsSecurityAgentPresent() { csecData := newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE", "") txn.SetCsecAttributes("CSEC_DATA", csecData) } return ctx, func(res *graphql.Result) { // noticing here also captures those during resolve for _, err := range res.Errors { txn.NoticeError(err) } seg.End() } } // ResolveFieldDidStart is called at the start of the resolving of a field func (Extension) ResolveFieldDidStart(ctx context.Context, i *graphql.ResolveInfo) (context.Context, graphql.ResolveFieldFinishFunc) { seg := newrelic.FromContext(ctx).StartSegment("ResolveField:" + i.FieldName) if newrelic.IsSecurityAgentPresent() { txn := newrelic.FromContext(ctx) csecData := txn.GetCsecAttributes()["CSEC_DATA"] newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE_LINKER", csecData) } return ctx, func(interface{}, error) { if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE_END", "") } seg.End() } } // HasResult returns true if the extension wants to add data to the result - this extension does not. func (Extension) HasResult() bool { return false } // GetResult returns the data that the extension wants to add to the result - in this case, none func (Extension) GetResult(context.Context) interface{} { return nil } go-agent-3.42.0/v3/integrations/nrgraphqlgo/nrgraphqlgo_test.go000066400000000000000000000236351510742411500246060ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgraphqlgo import ( "context" "encoding/json" "errors" "testing" "github.com/graphql-go/graphql" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) var schema = func() graphql.Schema { schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ Name: "RootQuery", Fields: graphql.Fields{ "hello": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "world", nil }, }, "errors": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return nil, errors.New("ooooooops") }, }, }, }), Extensions: []graphql.Extension{Extension{}}, }) if err != nil { panic(err) } return schema }() func TestExtensionNoTransaction(t *testing.T) { query := `{ hello }` params := graphql.Params{Schema: schema, RequestString: query} resp := graphql.Do(params) for _, err := range resp.Errors { t.Error("failure to Do:", err) } js, err := json.Marshal(resp.Data) if err != nil { t.Error("failure to marshal json:", err) } if data := string(js); data != `{"hello":"world"}` { t.Error("incorrect response data:", data) } } func TestExtensionWithTransaction(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("query") ctx := newrelic.NewContext(context.Background(), txn) query := `{ hello }` params := graphql.Params{ Schema: schema, RequestString: query, Context: ctx, } resp := graphql.Do(params) for _, err := range resp.Errors { t.Error("failure to Do:", err) } js, err := json.Marshal(resp.Data) if err != nil { t.Error("failure to marshal json:", err) } if data := string(js); data != `{"hello":"world"}` { t.Error("incorrect response data:", data) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/Execution", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Execution", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/ResolveField:hello", Scope: "", Forced: false, Data: nil}, {Name: "Custom/ResolveField:hello", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/query", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) } func TestExtensionResolveError(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("query") ctx := newrelic.NewContext(context.Background(), txn) query := `{ hello errors }` params := graphql.Params{ Schema: schema, RequestString: query, Context: ctx, } resp := graphql.Do(params) if len(resp.Errors) != 1 { t.Error("incorrect number of errors on response", resp.Errors) } js, err := json.Marshal(resp.Data) if err != nil { t.Error("failure to marshal json:", err) } if data := string(js); data != `{"errors":null,"hello":"world"}` { t.Error("incorrect response data:", data) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/Execution", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Execution", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/ResolveField:hello", Scope: "", Forced: false, Data: nil}, {Name: "Custom/ResolveField:hello", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/ResolveField:errors", Scope: "", Forced: false, Data: nil}, {Name: "Custom/ResolveField:errors", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Errors/OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/query", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": "ooooooops", "error.class": internal.MatchAnything, "transactionName": "OtherTransaction/Go/query", "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, }}) } func TestExtensionParseError(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("query") ctx := newrelic.NewContext(context.Background(), txn) query := `purple` params := graphql.Params{ Schema: schema, RequestString: query, Context: ctx, } resp := graphql.Do(params) if len(resp.Errors) != 1 { t.Error("incorrect number of errors on response", resp.Errors) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/Parse", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Errors/OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/query", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": internal.MatchAnything, "error.class": internal.MatchAnything, "transactionName": "OtherTransaction/Go/query", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, }}) } func TestExtensionValidationError(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("query") ctx := newrelic.NewContext(context.Background(), txn) query := `{ goodbye }` params := graphql.Params{ Schema: schema, RequestString: query, Context: ctx, } resp := graphql.Do(params) if len(resp.Errors) != 1 { t.Error("incorrect number of errors on response", resp.Errors) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/Parse", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Parse", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Validation", Scope: "OtherTransaction/Go/query", Forced: false, Data: nil}, {Name: "Errors/OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/Go/query", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/query", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": internal.MatchAnything, "error.class": internal.MatchAnything, "transactionName": "OtherTransaction/Go/query", "sampled": false, "guid": "*", "traceId": "*", "priority": "*", }, }}) } go-agent-3.42.0/v3/integrations/nrgrpc/000077500000000000000000000000001510742411500176325ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/LICENSE.txt000066400000000000000000000264501510742411500214640ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrgrpc/README.md000066400000000000000000000006601510742411500211130ustar00rootroot00000000000000# v3/integrations/nrgrpc [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgrpc?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgrpc) Package `nrgrpc` instruments https://github.com/grpc/grpc-go. ```go import "github.com/newrelic/go-agent/v3/integrations/nrgrpc" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrgrpc). go-agent-3.42.0/v3/integrations/nrgrpc/example/000077500000000000000000000000001510742411500212655ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/example/client/000077500000000000000000000000001510742411500225435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/example/client/client.go000066400000000000000000000053641510742411500243600ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "io" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrgrpc" sampleapp "github.com/newrelic/go-agent/v3/integrations/nrgrpc/example/sampleapp" newrelic "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" ) func doUnaryUnary(ctx context.Context, client sampleapp.SampleApplicationClient) { msg, err := client.DoUnaryUnary(ctx, &sampleapp.Message{Text: "Hello DoUnaryUnary"}) if err != nil { panic(err) } fmt.Println(msg.Text) } func doUnaryStream(ctx context.Context, client sampleapp.SampleApplicationClient) { stream, err := client.DoUnaryStream(ctx, &sampleapp.Message{Text: "Hello DoUnaryStream"}) if err != nil { panic(err) } for { msg, err := stream.Recv() if err == io.EOF { break } if err != nil { panic(err) } fmt.Println(msg.Text) } } func doStreamUnary(ctx context.Context, client sampleapp.SampleApplicationClient) { stream, err := client.DoStreamUnary(ctx) if err != nil { panic(err) } for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } panic(err) } } msg, err := stream.CloseAndRecv() if err != nil { panic(err) } fmt.Println(msg.Text) } func doStreamStream(ctx context.Context, client sampleapp.SampleApplicationClient) { stream, err := client.DoStreamStream(ctx) if err != nil { panic(err) } waitc := make(chan struct{}) go func() { for { msg, err := stream.Recv() if err == io.EOF { close(waitc) return } if err != nil { panic(err) } fmt.Println(msg.Text) } }() for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello DoStreamStream"}); err != nil { panic(err) } } stream.CloseSend() <-waitc } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("gRPC Client"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } defer app.Shutdown(10 * time.Second) app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("main") defer txn.End() conn, err := grpc.Dial( "localhost:8080", grpc.WithInsecure(), // Add the New Relic gRPC client instrumentation grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor), grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor), ) if err != nil { panic(err) } defer conn.Close() client := sampleapp.NewSampleApplicationClient(conn) ctx := newrelic.NewContext(context.Background(), txn) doUnaryUnary(ctx, client) doUnaryStream(ctx, client) doStreamUnary(ctx, client) doStreamStream(ctx, client) } go-agent-3.42.0/v3/integrations/nrgrpc/example/sampleapp/000077500000000000000000000000001510742411500232475ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/example/sampleapp/sampleapp.pb.go000066400000000000000000000276241510742411500261730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // source: sampleapp.proto package sampleapp import ( context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type Message struct { Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Message) Reset() { *m = Message{} } func (m *Message) String() string { return proto.CompactTextString(m) } func (*Message) ProtoMessage() {} func (*Message) Descriptor() ([]byte, []int) { return fileDescriptor_38ae74b4e52ac4e0, []int{0} } func (m *Message) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Message.Unmarshal(m, b) } func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Message.Marshal(b, m, deterministic) } func (m *Message) XXX_Merge(src proto.Message) { xxx_messageInfo_Message.Merge(m, src) } func (m *Message) XXX_Size() int { return xxx_messageInfo_Message.Size(m) } func (m *Message) XXX_DiscardUnknown() { xxx_messageInfo_Message.DiscardUnknown(m) } var xxx_messageInfo_Message proto.InternalMessageInfo func (m *Message) GetText() string { if m != nil { return m.Text } return "" } func init() { proto.RegisterType((*Message)(nil), "Message") } func init() { proto.RegisterFile("sampleapp.proto", fileDescriptor_38ae74b4e52ac4e0) } var fileDescriptor_38ae74b4e52ac4e0 = []byte{ // 153 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2f, 0x4e, 0xcc, 0x2d, 0xc8, 0x49, 0x4d, 0x2c, 0x28, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x92, 0xe5, 0x62, 0xf7, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 0x4f, 0x15, 0x12, 0xe2, 0x62, 0x29, 0x49, 0xad, 0x28, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x8d, 0xb6, 0x33, 0x72, 0x09, 0x06, 0x83, 0xb5, 0x38, 0x16, 0x14, 0xe4, 0x64, 0x26, 0x27, 0x96, 0x64, 0xe6, 0xe7, 0x09, 0xa9, 0x70, 0xf1, 0xb8, 0xe4, 0x87, 0xe6, 0x25, 0x16, 0x55, 0x82, 0x09, 0x21, 0x0e, 0x3d, 0xa8, 0x19, 0x52, 0x70, 0x96, 0x12, 0x83, 0x90, 0x3a, 0x17, 0x2f, 0x54, 0x55, 0x70, 0x49, 0x51, 0x6a, 0x62, 0x2e, 0x76, 0x65, 0x06, 0x8c, 0x10, 0x85, 0x10, 0x35, 0x78, 0xcc, 0xd3, 0x60, 0x14, 0xd2, 0xe2, 0xe2, 0x83, 0x29, 0xc4, 0x67, 0xa4, 0x06, 0xa3, 0x01, 0x63, 0x12, 0x1b, 0xd8, 0x7f, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x8b, 0x56, 0x80, 0xf2, 0x00, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // SampleApplicationClient is the client API for SampleApplication service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type SampleApplicationClient interface { DoUnaryUnary(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) DoUnaryStream(ctx context.Context, in *Message, opts ...grpc.CallOption) (SampleApplication_DoUnaryStreamClient, error) DoStreamUnary(ctx context.Context, opts ...grpc.CallOption) (SampleApplication_DoStreamUnaryClient, error) DoStreamStream(ctx context.Context, opts ...grpc.CallOption) (SampleApplication_DoStreamStreamClient, error) } type sampleApplicationClient struct { cc *grpc.ClientConn } func NewSampleApplicationClient(cc *grpc.ClientConn) SampleApplicationClient { return &sampleApplicationClient{cc} } func (c *sampleApplicationClient) DoUnaryUnary(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) { out := new(Message) err := c.cc.Invoke(ctx, "/SampleApplication/DoUnaryUnary", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *sampleApplicationClient) DoUnaryStream(ctx context.Context, in *Message, opts ...grpc.CallOption) (SampleApplication_DoUnaryStreamClient, error) { stream, err := c.cc.NewStream(ctx, &_SampleApplication_serviceDesc.Streams[0], "/SampleApplication/DoUnaryStream", opts...) if err != nil { return nil, err } x := &sampleApplicationDoUnaryStreamClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SampleApplication_DoUnaryStreamClient interface { Recv() (*Message, error) grpc.ClientStream } type sampleApplicationDoUnaryStreamClient struct { grpc.ClientStream } func (x *sampleApplicationDoUnaryStreamClient) Recv() (*Message, error) { m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *sampleApplicationClient) DoStreamUnary(ctx context.Context, opts ...grpc.CallOption) (SampleApplication_DoStreamUnaryClient, error) { stream, err := c.cc.NewStream(ctx, &_SampleApplication_serviceDesc.Streams[1], "/SampleApplication/DoStreamUnary", opts...) if err != nil { return nil, err } x := &sampleApplicationDoStreamUnaryClient{stream} return x, nil } type SampleApplication_DoStreamUnaryClient interface { Send(*Message) error CloseAndRecv() (*Message, error) grpc.ClientStream } type sampleApplicationDoStreamUnaryClient struct { grpc.ClientStream } func (x *sampleApplicationDoStreamUnaryClient) Send(m *Message) error { return x.ClientStream.SendMsg(m) } func (x *sampleApplicationDoStreamUnaryClient) CloseAndRecv() (*Message, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *sampleApplicationClient) DoStreamStream(ctx context.Context, opts ...grpc.CallOption) (SampleApplication_DoStreamStreamClient, error) { stream, err := c.cc.NewStream(ctx, &_SampleApplication_serviceDesc.Streams[2], "/SampleApplication/DoStreamStream", opts...) if err != nil { return nil, err } x := &sampleApplicationDoStreamStreamClient{stream} return x, nil } type SampleApplication_DoStreamStreamClient interface { Send(*Message) error Recv() (*Message, error) grpc.ClientStream } type sampleApplicationDoStreamStreamClient struct { grpc.ClientStream } func (x *sampleApplicationDoStreamStreamClient) Send(m *Message) error { return x.ClientStream.SendMsg(m) } func (x *sampleApplicationDoStreamStreamClient) Recv() (*Message, error) { m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // SampleApplicationServer is the server API for SampleApplication service. type SampleApplicationServer interface { DoUnaryUnary(context.Context, *Message) (*Message, error) DoUnaryStream(*Message, SampleApplication_DoUnaryStreamServer) error DoStreamUnary(SampleApplication_DoStreamUnaryServer) error DoStreamStream(SampleApplication_DoStreamStreamServer) error } // UnimplementedSampleApplicationServer can be embedded to have forward compatible implementations. type UnimplementedSampleApplicationServer struct { } func (*UnimplementedSampleApplicationServer) DoUnaryUnary(ctx context.Context, req *Message) (*Message, error) { return nil, status.Errorf(codes.Unimplemented, "method DoUnaryUnary not implemented") } func (*UnimplementedSampleApplicationServer) DoUnaryStream(req *Message, srv SampleApplication_DoUnaryStreamServer) error { return status.Errorf(codes.Unimplemented, "method DoUnaryStream not implemented") } func (*UnimplementedSampleApplicationServer) DoStreamUnary(srv SampleApplication_DoStreamUnaryServer) error { return status.Errorf(codes.Unimplemented, "method DoStreamUnary not implemented") } func (*UnimplementedSampleApplicationServer) DoStreamStream(srv SampleApplication_DoStreamStreamServer) error { return status.Errorf(codes.Unimplemented, "method DoStreamStream not implemented") } func RegisterSampleApplicationServer(s *grpc.Server, srv SampleApplicationServer) { s.RegisterService(&_SampleApplication_serviceDesc, srv) } func _SampleApplication_DoUnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Message) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SampleApplicationServer).DoUnaryUnary(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/SampleApplication/DoUnaryUnary", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SampleApplicationServer).DoUnaryUnary(ctx, req.(*Message)) } return interceptor(ctx, in, info, handler) } func _SampleApplication_DoUnaryStream_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(Message) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SampleApplicationServer).DoUnaryStream(m, &sampleApplicationDoUnaryStreamServer{stream}) } type SampleApplication_DoUnaryStreamServer interface { Send(*Message) error grpc.ServerStream } type sampleApplicationDoUnaryStreamServer struct { grpc.ServerStream } func (x *sampleApplicationDoUnaryStreamServer) Send(m *Message) error { return x.ServerStream.SendMsg(m) } func _SampleApplication_DoStreamUnary_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(SampleApplicationServer).DoStreamUnary(&sampleApplicationDoStreamUnaryServer{stream}) } type SampleApplication_DoStreamUnaryServer interface { SendAndClose(*Message) error Recv() (*Message, error) grpc.ServerStream } type sampleApplicationDoStreamUnaryServer struct { grpc.ServerStream } func (x *sampleApplicationDoStreamUnaryServer) SendAndClose(m *Message) error { return x.ServerStream.SendMsg(m) } func (x *sampleApplicationDoStreamUnaryServer) Recv() (*Message, error) { m := new(Message) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _SampleApplication_DoStreamStream_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(SampleApplicationServer).DoStreamStream(&sampleApplicationDoStreamStreamServer{stream}) } type SampleApplication_DoStreamStreamServer interface { Send(*Message) error Recv() (*Message, error) grpc.ServerStream } type sampleApplicationDoStreamStreamServer struct { grpc.ServerStream } func (x *sampleApplicationDoStreamStreamServer) Send(m *Message) error { return x.ServerStream.SendMsg(m) } func (x *sampleApplicationDoStreamStreamServer) Recv() (*Message, error) { m := new(Message) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } var _SampleApplication_serviceDesc = grpc.ServiceDesc{ ServiceName: "SampleApplication", HandlerType: (*SampleApplicationServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "DoUnaryUnary", Handler: _SampleApplication_DoUnaryUnary_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "DoUnaryStream", Handler: _SampleApplication_DoUnaryStream_Handler, ServerStreams: true, }, { StreamName: "DoStreamUnary", Handler: _SampleApplication_DoStreamUnary_Handler, ClientStreams: true, }, { StreamName: "DoStreamStream", Handler: _SampleApplication_DoStreamStream_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "sampleapp.proto", } go-agent-3.42.0/v3/integrations/nrgrpc/example/sampleapp/sampleapp.proto000066400000000000000000000006561510742411500263250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; service SampleApplication { rpc DoUnaryUnary(Message) returns (Message) {} rpc DoUnaryStream(Message) returns (stream Message) {} rpc DoStreamUnary(stream Message) returns (Message) {} rpc DoStreamStream(stream Message) returns (stream Message) {} } message Message { string text = 1; } go-agent-3.42.0/v3/integrations/nrgrpc/example/server/000077500000000000000000000000001510742411500225735ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/example/server/server.go000066400000000000000000000051661510742411500244400ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" fmt "fmt" "io" "net" "os" "github.com/newrelic/go-agent/v3/integrations/nrgrpc" sampleapp "github.com/newrelic/go-agent/v3/integrations/nrgrpc/example/sampleapp" "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" ) // Server is a gRPC server. type Server struct{} // processMessage processes each incoming Message. func processMessage(ctx context.Context, msg *sampleapp.Message) { defer newrelic.FromContext(ctx).StartSegment("processMessage").End() fmt.Printf("Message received: %s\n", msg.Text) } // DoUnaryUnary is a unary request, unary response method. func (s *Server) DoUnaryUnary(ctx context.Context, msg *sampleapp.Message) (*sampleapp.Message, error) { processMessage(ctx, msg) return &sampleapp.Message{Text: "Hello from DoUnaryUnary"}, nil } // DoUnaryStream is a unary request, stream response method. func (s *Server) DoUnaryStream(msg *sampleapp.Message, stream sampleapp.SampleApplication_DoUnaryStreamServer) error { processMessage(stream.Context(), msg) for i := 0; i < 3; i++ { if err := stream.Send(&sampleapp.Message{Text: "Hello from DoUnaryStream"}); nil != err { return err } } return nil } // DoStreamUnary is a stream request, unary response method. func (s *Server) DoStreamUnary(stream sampleapp.SampleApplication_DoStreamUnaryServer) error { for { msg, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&sampleapp.Message{Text: "Hello from DoStreamUnary"}) } else if nil != err { return err } processMessage(stream.Context(), msg) } } // DoStreamStream is a stream request, stream response method. func (s *Server) DoStreamStream(stream sampleapp.SampleApplication_DoStreamStreamServer) error { for { msg, err := stream.Recv() if err == io.EOF { return nil } else if nil != err { return err } processMessage(stream.Context(), msg) if err := stream.Send(&sampleapp.Message{Text: "Hello from DoStreamStream"}); nil != err { return err } } } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("gRPC Server"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } lis, err := net.Listen("tcp", "localhost:8080") if err != nil { panic(err) } grpcServer := grpc.NewServer( grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), ) sampleapp.RegisterSampleApplicationServer(grpcServer, &Server{}) grpcServer.Serve(lis) } go-agent-3.42.0/v3/integrations/nrgrpc/go.mod000066400000000000000000000012041510742411500207350ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrgrpc go 1.24 require ( // protobuf v1.3.0 is the earliest version using modules, we use v1.3.1 // because all dependencies were removed in this version. github.com/golang/protobuf v1.5.4 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrsecurityagent v1.1.0 // v1.15.0 is the earliest version of grpc using modules. google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) replace github.com/newrelic/go-agent/v3/integrations/nrsecurityagent => ../../integrations/nrsecurityagent replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrgrpc/nrgrpc_client.go000066400000000000000000000116431510742411500230170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgrpc import ( "context" "io" "net/http" "net/url" "strings" "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) func getURL(method, target string) *url.URL { var host string // target can be anything from // https://github.com/grpc/grpc/blob/master/doc/naming.md // see https://godoc.org/google.golang.org/grpc#DialContext if strings.HasPrefix(target, "unix:") { host = "localhost" } else { host = strings.TrimPrefix(target, "dns:///") } return &url.URL{ Scheme: "grpc", Host: host, Path: method, } } func getDummyRequest(method, target string) (request *http.Request) { request = &http.Request{} request.URL = getURL(method, target) request.Header = http.Header{} return request } // startClientSegment starts an ExternalSegment and adds Distributed Trace // headers to the outgoing grpc metadata in the context. func startClientSegment(ctx context.Context, method, target string) (*newrelic.ExternalSegment, context.Context) { var seg *newrelic.ExternalSegment var req *http.Request if txn := newrelic.FromContext(ctx); txn != nil { if newrelic.IsSecurityAgentPresent() { req = getDummyRequest(method, target) } seg = newrelic.StartExternalSegment(txn, req) method = strings.TrimPrefix(method, "/") seg.Host = getURL(method, target).Host seg.Library = "gRPC" seg.Procedure = method hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) > 0 { md, ok := metadata.FromOutgoingContext(ctx) if !ok { md = metadata.New(nil) } for k := range hdrs { if v := hdrs.Get(k); v != "" { md.Set(k, v) } } if newrelic.IsSecurityAgentPresent() { for k := range req.Header { if v := req.Header.Get(k); v != "" { md.Set(k, v) } } } ctx = metadata.NewOutgoingContext(ctx, md) } } return seg, ctx } // UnaryClientInterceptor instruments client unary RPCs. This interceptor // records each unary call with an external segment. Using it requires two steps: // // 1. Use this function with grpc.WithChainUnaryInterceptor or // grpc.WithUnaryInterceptor when creating a grpc.ClientConn. Example: // // conn, err := grpc.Dial( // "localhost:8080", // grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor), // grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor), // ) // // 2. Ensure that calls made with this grpc.ClientConn are done with a context // which contains a newrelic.Transaction. // // Full example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/client/client.go // // This interceptor only instruments unary calls. You must use both // UnaryClientInterceptor and StreamClientInterceptor to instrument unary and // streaming calls. These interceptors add headers to the call metadata if // distributed tracing is enabled. func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { seg, ctx := startClientSegment(ctx, method, cc.Target()) defer seg.End() return invoker(ctx, method, req, reply, cc, opts...) } type wrappedClientStream struct { grpc.ClientStream segment *newrelic.ExternalSegment isUnaryServer bool } func (s wrappedClientStream) RecvMsg(m interface{}) error { err := s.ClientStream.RecvMsg(m) if err == io.EOF || s.isUnaryServer { s.segment.End() } return err } // StreamClientInterceptor instruments client streaming RPCs. This interceptor // records streaming each call with an external segment. Using it requires two steps: // // 1. Use this function with grpc.WithChainStreamInterceptor or // grpc.WithStreamInterceptor when creating a grpc.ClientConn. Example: // // conn, err := grpc.Dial( // "localhost:8080", // grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor), // grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor), // ) // // 2. Ensure that calls made with this grpc.ClientConn are done with a context // which contains a newrelic.Transaction. // // Full example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/client/client.go // // This interceptor only instruments streaming calls. You must use both // UnaryClientInterceptor and StreamClientInterceptor to instrument unary and // streaming calls. These interceptors add headers to the call metadata if // distributed tracing is enabled. func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { seg, ctx := startClientSegment(ctx, method, cc.Target()) s, err := streamer(ctx, desc, cc, method, opts...) if err != nil { return s, err } return wrappedClientStream{ segment: seg, ClientStream: s, isUnaryServer: !desc.ServerStreams, }, nil } go-agent-3.42.0/v3/integrations/nrgrpc/nrgrpc_client_test.go000066400000000000000000000513151510742411500240560ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgrpc import ( "context" "encoding/json" "fmt" "io" "testing" "github.com/newrelic/go-agent/v3/integrations/nrgrpc/testapp" "github.com/newrelic/go-agent/v3/integrations/nrsecurityagent" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "google.golang.org/grpc/metadata" ) func TestGetURL(t *testing.T) { testcases := []struct { method string target string expected string }{ { method: "/TestApplication/DoUnaryUnary", target: "", expected: "grpc:///TestApplication/DoUnaryUnary", }, { method: "TestApplication/DoUnaryUnary", target: "", expected: "grpc://TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: ":8080", expected: "grpc://:8080/TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: "localhost:8080", expected: "grpc://localhost:8080/TestApplication/DoUnaryUnary", }, { method: "TestApplication/DoUnaryUnary", target: "localhost:8080", expected: "grpc://localhost:8080/TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: "dns:///localhost:8080", expected: "grpc://localhost:8080/TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: "unix:/path/to/socket", expected: "grpc://localhost/TestApplication/DoUnaryUnary", }, { method: "/TestApplication/DoUnaryUnary", target: "unix:///path/to/socket", expected: "grpc://localhost/TestApplication/DoUnaryUnary", }, } for _, test := range testcases { actual := getURL(test.method, test.target) if actual.String() != test.expected { t.Errorf("incorrect URL:\n\tmethod=%s,\n\ttarget=%s,\n\texpected=%s,\n\tactual=%s", test.method, test.target, test.expected, actual.String()) } } } func testApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.AccountID = "123" reply.TrustedAccountKey = "123" reply.PrimaryAppID = "456" } func TestUnaryClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("UnaryUnary") ctx := newrelic.NewContext(context.Background(), txn) s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(ctx, &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/UnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/UnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/gRPC/TestApplication/DoUnaryUnary", Scope: "OtherTransaction/Go/UnaryUnary", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "gRPC", "name": "External/bufnet/gRPC/TestApplication/DoUnaryUnary", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/UnaryUnary", "transaction.name": "OtherTransaction/Go/UnaryUnary", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/UnaryUnary", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/UnaryUnary", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/bufnet/gRPC/TestApplication/DoUnaryUnary", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestUnaryStreamClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("UnaryStream") ctx := newrelic.NewContext(context.Background(), txn) s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoUnaryStream(ctx, &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int for { msg, err := stream.Recv() if err == io.EOF { break } if err != nil { t.Fatal("error receiving message", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } recved++ } if recved != 3 { t.Fatal("received incorrect number of messages from server", recved) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/UnaryStream", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/UnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/gRPC/TestApplication/DoUnaryStream", Scope: "OtherTransaction/Go/UnaryStream", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "gRPC", "name": "External/bufnet/gRPC/TestApplication/DoUnaryStream", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/UnaryStream", "transaction.name": "OtherTransaction/Go/UnaryStream", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/UnaryStream", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/UnaryStream", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/bufnet/gRPC/TestApplication/DoUnaryStream", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestStreamUnaryClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("StreamUnary") ctx := newrelic.NewContext(context.Background(), txn) s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(ctx) if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } t.Fatal("failure to Send", err) } } msg, err := stream.CloseAndRecv() if err != nil { t.Fatal("failure to CloseAndRecv", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { t.Error("distributed trace header not sent", hdrs) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/StreamUnary", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/StreamUnary", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/gRPC/TestApplication/DoStreamUnary", Scope: "OtherTransaction/Go/StreamUnary", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "gRPC", "name": "External/bufnet/gRPC/TestApplication/DoStreamUnary", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/StreamUnary", "transaction.name": "OtherTransaction/Go/StreamUnary", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/StreamUnary", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/StreamUnary", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/bufnet/gRPC/TestApplication/DoStreamUnary", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestStreamStreamClientInterceptor(t *testing.T) { app := testApp() txn := app.StartTransaction("StreamStream") ctx := newrelic.NewContext(context.Background(), txn) s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamStream(ctx) if err != nil { t.Fatal("client call to DoStreamStream failed", err) } errC := make(chan error) go func(errC chan error) { var recved int for { msg, err := stream.Recv() if err == io.EOF { break } if err != nil { errC <- fmt.Errorf("failure to Recv: %v", err) return } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) if err != nil { errC <- fmt.Errorf("cannot unmarshall client response: %v", err) return } if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" { errC <- fmt.Errorf("distributed trace header not sent: %v", hdrs) return } recved++ } if recved != 3 { errC <- fmt.Errorf("received incorrect number of messages from server: %v", recved) return } errC <- nil }(errC) for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamStream"}); err != nil { t.Fatal("failure to Send", err) } } stream.CloseSend() err = <-errC close(errC) if err != nil { t.Fatal(err) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/StreamStream", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/StreamStream", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/gRPC/TestApplication/DoStreamStream", Scope: "OtherTransaction/Go/StreamStream", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "gRPC", "name": "External/bufnet/gRPC/TestApplication/DoStreamStream", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/StreamStream", "transaction.name": "OtherTransaction/Go/StreamStream", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/StreamStream", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/StreamStream", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/bufnet/gRPC/TestApplication/DoStreamStream", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestClientUnaryMetadata(t *testing.T) { // Test that metadata on the outgoing request are presevered app := testApp() txn := app.StartTransaction("metadata") ctx := newrelic.NewContext(context.Background(), txn) md := metadata.New(map[string]string{ "testing": "hello world", "newrelic": "payload", }) ctx = metadata.NewOutgoingContext(ctx, md) s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(ctx, &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || hdr[0] == "" || hdr[0] == "payload" { t.Error("distributed trace header not sent", hdrs) } if hdr, ok := hdrs["testing"]; !ok || len(hdr) != 1 || hdr[0] != "hello world" { t.Error("testing header not sent", hdrs) } } func TestNilTxnClientUnary(t *testing.T) { s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) resp, err := client.DoUnaryUnary(context.Background(), &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryUnary failed", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(resp.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if _, ok := hdrs["newrelic"]; ok { t.Error("distributed trace header sent", hdrs) } } func TestNilTxnClientStreaming(t *testing.T) { s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(context.Background()) if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } t.Fatal("failure to Send", err) } } msg, err := stream.CloseAndRecv() if err != nil { t.Fatal("failure to CloseAndRecv", err) } var hdrs map[string][]string err = json.Unmarshal([]byte(msg.Text), &hdrs) if err != nil { t.Fatal("cannot unmarshall client response", err) } if _, ok := hdrs["newrelic"]; ok { t.Error("distributed trace header sent", hdrs) } } func TestClientStreamingError(t *testing.T) { // Test that when creating the stream returns an error, no external // segments are created app := testApp() txn := app.StartTransaction("UnaryStream") s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 0) defer cancel() ctx = newrelic.NewContext(ctx, txn) _, err := client.DoUnaryStream(ctx, &testapp.Message{}) if err == nil { t.Fatal("client call to DoUnaryStream did not return error") } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/UnaryStream", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/UnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/UnaryStream", "transaction.name": "OtherTransaction/Go/UnaryStream", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/UnaryStream", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/UnaryStream", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{}, }}, }, }}) } func TestClientSecurityAgentEnabled(t *testing.T) { app := testApp() err := nrsecurityagent.InitSecurityAgent(app.Application, nrsecurityagent.ConfigSecurityMode("IAST"), nrsecurityagent.ConfigSecurityValidatorServiceEndPointUrl("wss://csec.nr-data.net"), nrsecurityagent.ConfigSecurityEnable(true), ) if err != nil { t.Fatal("Could not setup the nrsecurityagent", err) } } go-agent-3.42.0/v3/integrations/nrgrpc/nrgrpc_doc.go000066400000000000000000000106061510742411500223040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrgrpc instruments https://github.com/grpc/grpc-go. // // This package can be used to instrument gRPC servers and gRPC clients. // // # Server // // To instrument a gRPC server, use UnaryServerInterceptor and // StreamServerInterceptor with your newrelic.Application to create server // interceptors to pass to grpc.NewServer. // // The results of these calls are reported as errors or as informational // messages (of levels OK, Info, Warning, or Error) based on the gRPC status // code they return. // // In the simplest case, simply add interceptors as in the following example: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("gRPC Server"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDebugLogger(os.Stdout), // ) // server := grpc.NewServer( // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), // grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), // ) // // The disposition of each, in terms of how to report each of the various // gRPC status codes, is determined by a built-in set of defaults: // // OK OK // Info AlreadyExists, Canceled, InvalidArgument, NotFound, // Unauthenticated // Warning Aborted, DeadlineExceeded, FailedPrecondition, OutOfRange, // PermissionDenied, ResourceExhausted, Unavailable // Error DataLoss, Internal, Unknown, Unimplemented // // These // may be overridden on a case-by-case basis using `WithStatusHandler()` // options to each `UnaryServerInterceptor()` or `StreamServerInterceptor()` // call, or globally via the `Configure()` function. // // For example, to report DeadlineExceeded as an error and NotFound // as a warning, for the UnaryInterceptor only: // // server := grpc.NewServer( // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app, // nrgrpc.WithStatusHandler(codes.DeadlineExceeded, nrgrpc.ErrorInterceptorStatusHandler), // nrgrpc.WithStatusHandler(codes.NotFound, nrgrpc.WarningInterceptorStatusHandler)), // grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), // ) // // If you wanted to make those two changes to the overall default behavior, so they // apply to all subsequently declared interceptors: // // nrgrpc.Configure( // nrgrpc.WithStatusHandler(codes.DeadlineExceeded, nrgrpc.ErrorInterceptorStatusHandler), // nrgrpc.WithStatusHandler(codes.NotFound, nrgrpc.WarningInterceptorStatusHandler), // ) // server := grpc.NewServer( // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), // grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), // ) // // In this case the new behavior for those two status codes applies to both interceptors. // // These interceptors create transactions for inbound calls. The transaction is // added to the call context and can be accessed in your method handlers // using newrelic.FromContext. // // // handler is your gRPC server handler. Access the currently running // // transaction using newrelic.FromContext. // func (s *Server) handler(ctx context.Context, msg *pb.Message) (*pb.Message, error) { // if err := processMsg(msg); err != nil { // txn := newrelic.FromContext(ctx) // txn.NoticeError(err) // return nil, err // } // return &pb.Message{Text: "Hello World!"}, nil // } // // Full server example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/server/server.go // // # Client // // To instrument a gRPC client, follow these two steps: // // 1. Use UnaryClientInterceptor and StreamClientInterceptor when creating a // grpc.ClientConn. Example: // // conn, err := grpc.Dial( // "localhost:8080", // grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor), // grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor), // ) // // 2. Ensure that calls made with this grpc.ClientConn are done with a context // which contains a newrelic.Transaction. // // // Add the currently running transaction to the context before making a // // client call. // ctx := newrelic.NewContext(context.Background(), txn) // msg, err := client.handler(ctx, &pb.Message{"Hello World"}) // // Full client example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/client/client.go package nrgrpc import "github.com/newrelic/go-agent/v3/internal" func init() { internal.TrackUsage("integration", "framework", "grpc") } go-agent-3.42.0/v3/integrations/nrgrpc/nrgrpc_server.go000066400000000000000000000373041510742411500230510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // This integration instruments grpc service calls via // UnaryServerInterceptor and StreamServerInterceptor functions. // // The results of these calls are reported as errors or as informational // messages (of levels OK, Info, Warning, or Error) based on the gRPC status // code they return. // // In the simplest case, simply add interceptors as in the following example: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("gRPC Server"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDebugLogger(os.Stdout), // ) // server := grpc.NewServer( // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), // grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), // ) // // The disposition of each, in terms of how to report each of the various // gRPC status codes, is determined by a built-in set of defaults. These // may be overridden on a case-by-case basis using WithStatusHandler // options to each UnaryServerInterceptor or StreamServerInterceptor // call, or globally via the Configure function. // // Full example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrgrpc/example/server/server.go // package nrgrpc import ( "context" "net/http" "strings" protoV1 "github.com/golang/protobuf/proto" "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" protoV2 "google.golang.org/protobuf/proto" ) func startTransaction(ctx context.Context, app *newrelic.Application, fullMethod string) *newrelic.Transaction { method := strings.TrimPrefix(fullMethod, "/") var hdrs http.Header if md, ok := metadata.FromIncomingContext(ctx); ok { hdrs = make(http.Header, len(md)) for k, vs := range md { for _, v := range vs { hdrs.Add(k, v) } } } target := hdrs.Get(":authority") url := getURL(method, target) transport := newrelic.TransportHTTP p, ok := peer.FromContext(ctx) if ok && p != nil && p.AuthInfo != nil && p.AuthInfo.AuthType() == "tls" { transport = newrelic.TransportHTTPS } webReq := newrelic.WebRequest{ Header: hdrs, URL: url, Method: method, Transport: transport, Type: "gRPC", ServerName: target, } txn := app.StartTransaction(method) if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, method) } txn.SetWebRequest(webReq) return txn } // ErrorHandler is the type of a gRPC status handler function. // Normally the supplied set of ErrorHandler functions will suffice, but // a custom handler may be crafted by the user and installed as a handler // if needed. type ErrorHandler func(context.Context, *newrelic.Transaction, *status.Status) // Internal registry of handlers associated with various // status codes. type statusHandlerMap map[codes.Code]ErrorHandler // interceptorStatusHandlerRegistry is the current default set of handlers // used by each interceptor. var interceptorStatusHandlerRegistry = statusHandlerMap{ codes.OK: OKInterceptorStatusHandler, codes.Canceled: InfoInterceptorStatusHandler, codes.Unknown: ErrorInterceptorStatusHandler, codes.InvalidArgument: InfoInterceptorStatusHandler, codes.DeadlineExceeded: WarningInterceptorStatusHandler, codes.NotFound: InfoInterceptorStatusHandler, codes.AlreadyExists: InfoInterceptorStatusHandler, codes.PermissionDenied: WarningInterceptorStatusHandler, codes.ResourceExhausted: WarningInterceptorStatusHandler, codes.FailedPrecondition: WarningInterceptorStatusHandler, codes.Aborted: WarningInterceptorStatusHandler, codes.OutOfRange: WarningInterceptorStatusHandler, codes.Unimplemented: ErrorInterceptorStatusHandler, codes.Internal: ErrorInterceptorStatusHandler, codes.Unavailable: WarningInterceptorStatusHandler, codes.DataLoss: ErrorInterceptorStatusHandler, codes.Unauthenticated: InfoInterceptorStatusHandler, } // HandlerOption is the type for options passed to the interceptor // functions to specify gRPC status handlers. type HandlerOption func(statusHandlerMap) // WithStatusHandler indicates a handler function to be used to // report the indicated gRPC status. Zero or more of these may be // given to the Configure, StreamServiceInterceptor, or // UnaryServiceInterceptor functions. // // The ErrorHandler parameter is generally one of the provided standard // reporting functions: // // OKInterceptorStatusHandler // report the operation as successful // ErrorInterceptorStatusHandler // report the operation as an error // WarningInterceptorStatusHandler // report the operation as a warning // InfoInterceptorStatusHandler // report the operation as an informational message // // The following reporting function should only be used if you know for sure // you want this. It does not report the error in any way at all, but completely // ignores it. // // IgnoreInterceptorStatusHandler // report the operation as successful // // Finally, if you have a custom reporting need that isn't covered by the standard // handler functions, you can create your own handler function as // // func myHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { // ... // } // // Within the function, do whatever you need to do with the txn parameter to report the // gRPC status passed as s. If needed, the context is also passed to your function. // // If you wish to use your custom handler for a code such as codes.NotFound, you would // include the parameter // // WithStatusHandler(codes.NotFound, myHandler) // // to your Configure, StreamServiceInterceptor, or UnaryServiceInterceptor function. func WithStatusHandler(c codes.Code, h ErrorHandler) HandlerOption { return func(m statusHandlerMap) { m[c] = h } } // Configure takes a list of WithStatusHandler options and sets them // as the new default handlers for the specified gRPC status codes, in the same // way as if WithStatusHandler were given to the StreamServiceInterceptor // or UnaryServiceInterceptor functions (q.v.); however, in this case the new handlers // become the default for any subsequent interceptors created by the above functions. func Configure(options ...HandlerOption) { for _, option := range options { option(interceptorStatusHandlerRegistry) } } // IgnoreInterceptorStatusHandler is our standard handler for // gRPC statuses which we want to ignore (in terms of any gRPC-specific // reporting on the transaction). func IgnoreInterceptorStatusHandler(_ context.Context, _ *newrelic.Transaction, _ *status.Status) {} // OKInterceptorStatusHandler is our standard handler for // gRPC statuses which we want to report as being successful, as with the // status code OK. // // This adds no additional attributes on the transaction other than // the fact that it was successful. func OKInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) } // ErrorInterceptorStatusHandler is our standard handler for // gRPC statuses which we want to report as being errors, // with the relevant error messages and // contextual information gleaned from the error value received from the RPC call. func ErrorInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) txn.NoticeError(&newrelic.Error{ Message: s.Message(), Class: "gRPC Status: " + s.Code().String(), }) txn.AddAttribute("grpcStatusLevel", "error") txn.AddAttribute("grpcStatusMessage", s.Message()) txn.AddAttribute("grpcStatusCode", s.Code().String()) } // WarningInterceptorStatusHandler is our standard handler for // gRPC statuses which we want to report as warnings. // // Reports the transaction's status with attributes containing information gleaned // from the error value returned, but does not count this as an error. func WarningInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) txn.AddAttribute("grpcStatusLevel", "warning") txn.AddAttribute("grpcStatusMessage", s.Message()) txn.AddAttribute("grpcStatusCode", s.Code().String()) } // InfoInterceptorStatusHandler is our standard handler for // gRPC statuses which we want to report as informational messages only. // // Reports the transaction's status with attributes containing information gleaned // from the error value returned, but does not count this as an error. func InfoInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction, s *status.Status) { txn.SetWebResponse(nil).WriteHeader(int(codes.OK)) txn.AddAttribute("grpcStatusLevel", "info") txn.AddAttribute("grpcStatusMessage", s.Message()) txn.AddAttribute("grpcStatusCode", s.Code().String()) } // DefaultInterceptorStatusHandler indicates which of our standard handlers // will be used for any status code which is not // explicitly assigned a handler. var DefaultInterceptorStatusHandler = InfoInterceptorStatusHandler // reportInterceptorStatus is the common routine for reporting any kind of interceptor. func reportInterceptorStatus(ctx context.Context, txn *newrelic.Transaction, handlers statusHandlerMap, err error) { grpcStatus := status.Convert(err) handler, ok := handlers[grpcStatus.Code()] if !ok { handler = DefaultInterceptorStatusHandler } handler(ctx, txn, grpcStatus) } // UnaryServerInterceptor instruments server unary RPCs. // // Use this function with grpc.UnaryInterceptor and a newrelic.Application to // create a grpc.ServerOption to pass to grpc.NewServer. This interceptor // records each unary call with a transaction. You must use both // UnaryServerInterceptor and StreamServerInterceptor to instrument unary and // streaming calls. // // Example: // // app, _ := newrelic.NewApplication( // newrelic.ConfigAppName("gRPC Server"), // newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), // newrelic.ConfigDebugLogger(os.Stdout), // ) // server := grpc.NewServer( // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), // grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), // ) // // These interceptors add the transaction to the call context so it may be // accessed in your method handlers using newrelic.FromContext. // // The nrgrpc integration has a built-in set of handlers for each gRPC status // code encountered. Serious errors are reported as error traces à la the // newrelic.NoticeError function, while the others are reported but not // counted as errors. // // If you wish to change this behavior, you may do so at a global level for // all intercepted functions by calling the Configure function, passing // any number of WithStatusHandler(code, handler) functions as parameters. // // You can specify a custom set of handlers with each interceptor creation by adding // WithStatusHandler calls at the end of the StreamInterceptor call's parameter list, // like so: // // grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app, // nrgrpc.WithStatusHandler(codes.OutOfRange, nrgrpc.WarningInterceptorStatusHandler), // nrgrpc.WithStatusHandler(codes.Unimplemented, nrgrpc.InfoInterceptorStatusHandler))) // // In this case, those two handlers are used (along with the current defaults for the other status // codes) only for that interceptor. func UnaryServerInterceptor(app *newrelic.Application, options ...HandlerOption) grpc.UnaryServerInterceptor { if app == nil { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return handler(ctx, req) } } localHandlerMap := make(statusHandlerMap) for code, handler := range interceptorStatusHandlerRegistry { localHandlerMap[code] = handler } for _, option := range options { option(localHandlerMap) } return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { txn := startTransaction(ctx, app, info.FullMethod) if newrelic.IsSecurityAgentPresent() { messageType, version := getMessageType(req) newrelic.GetSecurityAgentInterface().SendEvent("GRPC", req, messageType, version) } defer txn.End() ctx = newrelic.NewContext(ctx, txn) resp, err = handler(ctx, req) reportInterceptorStatus(ctx, txn, localHandlerMap, err) return } } type wrappedServerStream struct { grpc.ServerStream txn *newrelic.Transaction } func (s wrappedServerStream) Context() context.Context { ctx := s.ServerStream.Context() return newrelic.NewContext(ctx, s.txn) } func (s wrappedServerStream) RecvMsg(msg any) error { if newrelic.IsSecurityAgentPresent() { messageType, version := getMessageType(msg) newrelic.GetSecurityAgentInterface().SendEvent("GRPC", msg, messageType, version) } return s.ServerStream.RecvMsg(msg) } func newWrappedServerStream(stream grpc.ServerStream, txn *newrelic.Transaction) grpc.ServerStream { return wrappedServerStream{ ServerStream: stream, txn: txn, } } // StreamServerInterceptor instruments server streaming RPCs. // // Use this function with grpc.StreamInterceptor and a newrelic.Application to // create a grpc.ServerOption to pass to grpc.NewServer. This interceptor // records each streaming call with a transaction. You must use both // UnaryServerInterceptor and StreamServerInterceptor to instrument unary and // streaming calls. // // See the notes and examples for the UnaryServerInterceptor function. func StreamServerInterceptor(app *newrelic.Application, options ...HandlerOption) grpc.StreamServerInterceptor { if app == nil { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { return handler(srv, ss) } } localHandlerMap := make(statusHandlerMap) for code, handler := range interceptorStatusHandlerRegistry { localHandlerMap[code] = handler } for _, option := range options { option(localHandlerMap) } return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { txn := startTransaction(ss.Context(), app, info.FullMethod) defer txn.End() if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("GRPC_INFO", info.IsClientStream, info.IsServerStream) } err := handler(srv, newWrappedServerStream(ss, txn)) reportInterceptorStatus(ss.Context(), txn, localHandlerMap, err) return err } } // WrapRouter extracts API endpoints from the grpc server instance passed to it // which is used to detect application URL mapping(api-endpoints) for provable security. // In this version of the integration, this wrapper is only necessary if you are using the New Relic security agent integration [https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsecurityagent], // but it may be enhanced to provide additional functionality in future releases. // // grpcServer := grpc.NewServer(...) // .... // .... // .... // // nrgrpc.WrapRouter(grpcServer) func WrapRouter(server *grpc.Server) { if server != nil && newrelic.IsSecurityAgentPresent() { for n, info := range server.GetServiceInfo() { if info.Methods != nil { for i := range info.Methods { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", n+"/"+info.Methods[i].Name, "*", info.Methods[i].Name) } } } } } func getMessageType(req any) (string, string) { messageType := "" version := "v2" messagev2, ok := req.(protoV2.Message) if ok { messageType = string(messagev2.ProtoReflect().Descriptor().FullName()) } else { messagev1, ok := req.(protoV1.Message) if ok { messageType = string(protoV1.MessageReflect(messagev1).Descriptor().FullName()) version = "v1" } } return messageType, version } go-agent-3.42.0/v3/integrations/nrgrpc/nrgrpc_server_test.go000066400000000000000000001077121510742411500241110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrgrpc import ( "context" "fmt" "io" "net" "strings" "testing" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" "github.com/newrelic/go-agent/v3/integrations/nrgrpc/testapp" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) // newTestServerAndConn creates a new *grpc.Server and *grpc.ClientConn for use // in testing. It adds instrumentation to both. If app is nil, then // instrumentation is not applied to the server. Be sure to Stop() the server // and Close() the connection when done with them. func newTestServerAndConn(t *testing.T, app *newrelic.Application) (*grpc.Server, *grpc.ClientConn) { s := grpc.NewServer( grpc.UnaryInterceptor(UnaryServerInterceptor(app)), grpc.StreamInterceptor(StreamServerInterceptor(app)), ) testapp.RegisterTestApplicationServer(s, &testapp.Server{}) lis := bufconn.Listen(1024 * 1024) go func() { s.Serve(lis) }() bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } conn, err := grpc.Dial("bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), // create the connection synchronously grpc.WithUnaryInterceptor(UnaryClientInterceptor), grpc.WithStreamInterceptor(StreamClientInterceptor), ) if err != nil { t.Fatal("failure to create ClientConn", err) } return s, conn } func TestWithCustomStatusHandler(t *testing.T) { app := testApp() Configure(WithStatusHandler(codes.OK, WarningInterceptorStatusHandler)) s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) _, err := client.DoUnaryUnary(ctx, &testapp.Message{}) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "WebTransaction/Go/TestApplication/DoUnaryUnary", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "warning", "grpcStatusCode": "OK", "grpcStatusMessage": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoUnaryUnary", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "transaction.name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "warning", "grpcStatusCode": "OK", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }, }) Configure(WithStatusHandler(codes.OK, OKInterceptorStatusHandler)) } func TestWithInfoStatusHandler(t *testing.T) { app := testApp() Configure(WithStatusHandler(codes.OK, InfoInterceptorStatusHandler)) s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) _, err := client.DoUnaryUnary(ctx, &testapp.Message{}) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "WebTransaction/Go/TestApplication/DoUnaryUnary", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "info", "grpcStatusCode": "OK", "grpcStatusMessage": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoUnaryUnary", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "transaction.name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "info", "grpcStatusCode": "OK", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }, }) Configure(WithStatusHandler(codes.OK, OKInterceptorStatusHandler)) } func TestUnaryServerInterceptor(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) _, err := client.DoUnaryUnary(ctx, &testapp.Message{}) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryUnary", Scope: "WebTransaction/Go/TestApplication/DoUnaryUnary", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnary", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoUnaryUnary", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "transaction.name": "WebTransaction/Go/TestApplication/DoUnaryUnary", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnary", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnary", }, }, }) } func TestUnaryServerInterceptorError(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) _, err := client.DoUnaryUnaryError(context.Background(), &testapp.Message{}) if err == nil { t.Fatal("DoUnaryUnaryError should have returned an error") } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Errors/WebTransaction/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryUnaryError", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", "nr.apdexPerfZone": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusMessage": "oooooops!", "grpcStatusCode": "DataLoss", "grpcStatusLevel": "error", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnaryError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", }, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "gRPC Status: DataLoss", "error.message": "oooooops!", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "spanId": internal.MatchAnything, "traceId": internal.MatchAnything, "transactionName": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.User-Agent": internal.MatchAnything, "request.headers.userAgent": internal.MatchAnything, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryUnaryError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", }, UserAttributes: map[string]interface{}{ "grpcStatusMessage": "oooooops!", "grpcStatusCode": "DataLoss", "grpcStatusLevel": "error", }, }}) } func TestUnaryStreamServerInterceptor(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoUnaryStream(ctx, &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } var recved int for { _, err := stream.Recv() if err == io.EOF { break } if err != nil { t.Fatal("error receiving message", err) } recved++ } if recved != 3 { t.Fatal("received incorrect number of messages from server", recved) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoUnaryStream", Scope: "WebTransaction/Go/TestApplication/DoUnaryStream", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryStream", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryStream", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryStream", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStream", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStream", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoUnaryStream", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoUnaryStream", "transaction.name": "WebTransaction/Go/TestApplication/DoUnaryStream", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStream", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStream", }, }, }) } func TestStreamUnaryServerInterceptor(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoStreamUnary(ctx) if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } t.Fatal("failure to Send", err) } } _, err = stream.CloseAndRecv() if err != nil { t.Fatal("failure to CloseAndRecv", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoStreamUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoStreamUnary", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoStreamUnary", Scope: "WebTransaction/Go/TestApplication/DoStreamUnary", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoStreamUnary", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoStreamUnary", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoStreamUnary", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoStreamUnary", "request.uri": "grpc://bufnet/TestApplication/DoStreamUnary", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoStreamUnary", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoStreamUnary", "transaction.name": "WebTransaction/Go/TestApplication/DoStreamUnary", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoStreamUnary", "request.uri": "grpc://bufnet/TestApplication/DoStreamUnary", }, }, }) } func TestStreamStreamServerInterceptor(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) txn := app.StartTransaction("client") ctx := newrelic.NewContext(context.Background(), txn) stream, err := client.DoStreamStream(ctx) if err != nil { t.Fatal("client call to DoStreamStream failed", err) } errc := make(chan error) go func(errc chan error) { var recved int for { _, err := stream.Recv() if err == io.EOF { break } if err != nil { errc <- fmt.Errorf("failure to Recv: %v", err) return } recved++ } if recved != 3 { errc <- fmt.Errorf("received incorrect number of messages from server: %v", recved) return } errc <- nil }(errc) for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamStream"}); err != nil { t.Fatal("failure to Send", err) } } stream.CloseSend() err = <-errc close(errc) if err != nil { t.Fatal(err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoStreamStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoStreamStream", Scope: "", Forced: false, Data: nil}, {Name: "Custom/DoStreamStream", Scope: "WebTransaction/Go/TestApplication/DoStreamStream", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoStreamStream", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoStreamStream", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoStreamStream", "nr.apdexPerfZone": internal.MatchAnything, "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoStreamStream", "request.uri": "grpc://bufnet/TestApplication/DoStreamStream", }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/DoStreamStream", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestApplication/DoStreamStream", "transaction.name": "WebTransaction/Go/TestApplication/DoStreamStream", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoStreamStream", "request.uri": "grpc://bufnet/TestApplication/DoStreamStream", }, }, }) } func TestStreamServerInterceptorError(t *testing.T) { app := testApp() s, conn := newTestServerAndConn(t, app.Application) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoUnaryStreamError(context.Background(), &testapp.Message{}) if err != nil { t.Fatal("client call to DoUnaryStream failed", err) } _, err = stream.Recv() if err == nil { t.Fatal("DoUnaryStreamError should have returned an error") } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestApplication/DoUnaryStreamError", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Errors/WebTransaction/Go/TestApplication/DoUnaryStreamError", Scope: "", Forced: true, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestApplication/DoUnaryStreamError", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestApplication/DoUnaryStreamError", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "WebTransaction/Go/TestApplication/DoUnaryStreamError", "nr.apdexPerfZone": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "error", "grpcStatusMessage": "oooooops!", "grpcStatusCode": "DataLoss", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStreamError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", }, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "gRPC Status: DataLoss", "error.message": "oooooops!", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "spanId": internal.MatchAnything, "traceId": internal.MatchAnything, "transactionName": "WebTransaction/Go/TestApplication/DoUnaryStreamError", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 0, "http.statusCode": 0, "request.headers.User-Agent": internal.MatchAnything, "request.headers.userAgent": internal.MatchAnything, "request.headers.contentType": "application/grpc", "request.method": "TestApplication/DoUnaryStreamError", "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", }, UserAttributes: map[string]interface{}{ "grpcStatusLevel": "error", "grpcStatusMessage": "oooooops!", "grpcStatusCode": "DataLoss", }, }}) } func TestUnaryServerInterceptorNilApp(t *testing.T) { s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) msg, err := client.DoUnaryUnary(context.Background(), &testapp.Message{}) if err != nil { t.Fatal("unable to call client DoUnaryUnary", err) } if !strings.Contains(msg.Text, "content-type") { t.Error("incorrect message received") } } func TestStreamServerInterceptorNilApp(t *testing.T) { s, conn := newTestServerAndConn(t, nil) defer s.Stop() defer conn.Close() client := testapp.NewTestApplicationClient(conn) stream, err := client.DoStreamUnary(context.Background()) if err != nil { t.Fatal("client call to DoStreamUnary failed", err) } for i := 0; i < 3; i++ { if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); err != nil { if err == io.EOF { break } t.Fatal("failure to Send", err) } } msg, err := stream.CloseAndRecv() if err != nil { t.Fatal("failure to CloseAndRecv", err) } if !strings.Contains(msg.Text, "content-type") { t.Error("incorrect message received") } } func TestInterceptorsNilAppReturnNonNil(t *testing.T) { // When using the `grpc_middleware.WithUnaryServerChain` or // `grpc_middleware.WithStreamServerChain` options (see // https://godoc.org/github.com/grpc-ecosystem/go-grpc-middleware), calls // will panic if our intercepters return nil. uInt := UnaryServerInterceptor(nil) if uInt == nil { t.Error("UnaryServerInterceptor returned nil") } sInt := StreamServerInterceptor(nil) if sInt == nil { t.Error("StreamServerInterceptor returned nil") } } go-agent-3.42.0/v3/integrations/nrgrpc/testapp/000077500000000000000000000000001510742411500213125ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrgrpc/testapp/README.md000066400000000000000000000010601510742411500225660ustar00rootroot00000000000000# Testing gRPC Application This directory contains a testing application for validating the New Relic gRPC integration. The code in `testapp.pb.go` is generated using the following command (to be run from the `v3/integrations/nrgrpc` directory). This command should be rerun every time the `testapp.proto` file has changed for any reason. ```bash $ protoc -I testapp/ testapp/testapp.proto --go_out=plugins=grpc:testapp ``` To install required dependencies: ```bash go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/protoc-gen-go ``` go-agent-3.42.0/v3/integrations/nrgrpc/testapp/server.go000066400000000000000000000051301510742411500231460ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package testapp import ( "context" "encoding/json" "io" newrelic "github.com/newrelic/go-agent/v3/newrelic" codes "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" status "google.golang.org/grpc/status" ) // Server is a gRPC server. type Server struct{} // DoUnaryUnary is a unary request, unary response method. func (s *Server) DoUnaryUnary(ctx context.Context, msg *Message) (*Message, error) { defer newrelic.FromContext(ctx).StartSegment("DoUnaryUnary").End() md, _ := metadata.FromIncomingContext(ctx) js, _ := json.Marshal(md) return &Message{Text: string(js)}, nil } // DoUnaryStream is a unary request, stream response method. func (s *Server) DoUnaryStream(msg *Message, stream TestApplication_DoUnaryStreamServer) error { defer newrelic.FromContext(stream.Context()).StartSegment("DoUnaryStream").End() md, _ := metadata.FromIncomingContext(stream.Context()) js, _ := json.Marshal(md) for i := 0; i < 3; i++ { if err := stream.Send(&Message{Text: string(js)}); nil != err { return err } } return nil } // DoStreamUnary is a stream request, unary response method. func (s *Server) DoStreamUnary(stream TestApplication_DoStreamUnaryServer) error { defer newrelic.FromContext(stream.Context()).StartSegment("DoStreamUnary").End() md, _ := metadata.FromIncomingContext(stream.Context()) js, _ := json.Marshal(md) for { _, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&Message{Text: string(js)}) } else if nil != err { return err } } } // DoStreamStream is a stream request, stream response method. func (s *Server) DoStreamStream(stream TestApplication_DoStreamStreamServer) error { defer newrelic.FromContext(stream.Context()).StartSegment("DoStreamStream").End() md, _ := metadata.FromIncomingContext(stream.Context()) js, _ := json.Marshal(md) for { _, err := stream.Recv() if err == io.EOF { return nil } else if nil != err { return err } if err := stream.Send(&Message{Text: string(js)}); nil != err { return err } } } // DoUnaryUnaryError is a unary request, unary response method that returns an // error. func (s *Server) DoUnaryUnaryError(ctx context.Context, msg *Message) (*Message, error) { return &Message{}, status.New(codes.DataLoss, "oooooops!").Err() } // DoUnaryStreamError is a unary request, unary response method that returns an // error. func (s *Server) DoUnaryStreamError(msg *Message, stream TestApplication_DoUnaryStreamErrorServer) error { return status.New(codes.DataLoss, "oooooops!").Err() } go-agent-3.42.0/v3/integrations/nrgrpc/testapp/testapp.pb.go000066400000000000000000000335131510742411500237260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // source: testapp.proto package testapp import ( context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type Message struct { Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Message) Reset() { *m = Message{} } func (m *Message) String() string { return proto.CompactTextString(m) } func (*Message) ProtoMessage() {} func (*Message) Descriptor() ([]byte, []int) { return fileDescriptor_98d4e818d9f182b1, []int{0} } func (m *Message) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Message.Unmarshal(m, b) } func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Message.Marshal(b, m, deterministic) } func (m *Message) XXX_Merge(src proto.Message) { xxx_messageInfo_Message.Merge(m, src) } func (m *Message) XXX_Size() int { return xxx_messageInfo_Message.Size(m) } func (m *Message) XXX_DiscardUnknown() { xxx_messageInfo_Message.DiscardUnknown(m) } var xxx_messageInfo_Message proto.InternalMessageInfo func (m *Message) GetText() string { if m != nil { return m.Text } return "" } func init() { proto.RegisterType((*Message)(nil), "Message") } func init() { proto.RegisterFile("testapp.proto", fileDescriptor_98d4e818d9f182b1) } var fileDescriptor_98d4e818d9f182b1 = []byte{ // 175 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x49, 0x2d, 0x2e, 0x49, 0x2c, 0x28, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x92, 0xe5, 0x62, 0xf7, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 0x4f, 0x15, 0x12, 0xe2, 0x62, 0x29, 0x49, 0xad, 0x28, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x8d, 0xfa, 0x98, 0xb8, 0xf8, 0x43, 0x52, 0x8b, 0x4b, 0x1c, 0x0b, 0x0a, 0x72, 0x32, 0x93, 0x13, 0x4b, 0x32, 0xf3, 0xf3, 0x84, 0x54, 0xb8, 0x78, 0x5c, 0xf2, 0x43, 0xf3, 0x12, 0x8b, 0x2a, 0xc1, 0x84, 0x10, 0x87, 0x1e, 0xd4, 0x04, 0x29, 0x38, 0x4b, 0x89, 0x41, 0x48, 0x9d, 0x8b, 0x17, 0xaa, 0x2a, 0xb8, 0xa4, 0x28, 0x35, 0x31, 0x17, 0xbb, 0x32, 0x03, 0x46, 0x88, 0x42, 0x88, 0x1a, 0x3c, 0xe6, 0x69, 0x30, 0x0a, 0x69, 0x71, 0xf1, 0xc1, 0x14, 0xe2, 0x33, 0x52, 0x83, 0xd1, 0x80, 0x51, 0x48, 0x93, 0x4b, 0x10, 0xd9, 0x8d, 0xae, 0x45, 0x45, 0xf9, 0x45, 0x38, 0x1c, 0xaa, 0xc3, 0x25, 0x84, 0xe2, 0x50, 0x3c, 0x6a, 0x0d, 0x18, 0x93, 0xd8, 0xc0, 0xc1, 0x66, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xb8, 0xca, 0x5d, 0x32, 0x47, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // TestApplicationClient is the client API for TestApplication service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type TestApplicationClient interface { DoUnaryUnary(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) DoUnaryStream(ctx context.Context, in *Message, opts ...grpc.CallOption) (TestApplication_DoUnaryStreamClient, error) DoStreamUnary(ctx context.Context, opts ...grpc.CallOption) (TestApplication_DoStreamUnaryClient, error) DoStreamStream(ctx context.Context, opts ...grpc.CallOption) (TestApplication_DoStreamStreamClient, error) DoUnaryUnaryError(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) DoUnaryStreamError(ctx context.Context, in *Message, opts ...grpc.CallOption) (TestApplication_DoUnaryStreamErrorClient, error) } type testApplicationClient struct { cc *grpc.ClientConn } func NewTestApplicationClient(cc *grpc.ClientConn) TestApplicationClient { return &testApplicationClient{cc} } func (c *testApplicationClient) DoUnaryUnary(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) { out := new(Message) err := c.cc.Invoke(ctx, "/TestApplication/DoUnaryUnary", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *testApplicationClient) DoUnaryStream(ctx context.Context, in *Message, opts ...grpc.CallOption) (TestApplication_DoUnaryStreamClient, error) { stream, err := c.cc.NewStream(ctx, &_TestApplication_serviceDesc.Streams[0], "/TestApplication/DoUnaryStream", opts...) if err != nil { return nil, err } x := &testApplicationDoUnaryStreamClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type TestApplication_DoUnaryStreamClient interface { Recv() (*Message, error) grpc.ClientStream } type testApplicationDoUnaryStreamClient struct { grpc.ClientStream } func (x *testApplicationDoUnaryStreamClient) Recv() (*Message, error) { m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *testApplicationClient) DoStreamUnary(ctx context.Context, opts ...grpc.CallOption) (TestApplication_DoStreamUnaryClient, error) { stream, err := c.cc.NewStream(ctx, &_TestApplication_serviceDesc.Streams[1], "/TestApplication/DoStreamUnary", opts...) if err != nil { return nil, err } x := &testApplicationDoStreamUnaryClient{stream} return x, nil } type TestApplication_DoStreamUnaryClient interface { Send(*Message) error CloseAndRecv() (*Message, error) grpc.ClientStream } type testApplicationDoStreamUnaryClient struct { grpc.ClientStream } func (x *testApplicationDoStreamUnaryClient) Send(m *Message) error { return x.ClientStream.SendMsg(m) } func (x *testApplicationDoStreamUnaryClient) CloseAndRecv() (*Message, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *testApplicationClient) DoStreamStream(ctx context.Context, opts ...grpc.CallOption) (TestApplication_DoStreamStreamClient, error) { stream, err := c.cc.NewStream(ctx, &_TestApplication_serviceDesc.Streams[2], "/TestApplication/DoStreamStream", opts...) if err != nil { return nil, err } x := &testApplicationDoStreamStreamClient{stream} return x, nil } type TestApplication_DoStreamStreamClient interface { Send(*Message) error Recv() (*Message, error) grpc.ClientStream } type testApplicationDoStreamStreamClient struct { grpc.ClientStream } func (x *testApplicationDoStreamStreamClient) Send(m *Message) error { return x.ClientStream.SendMsg(m) } func (x *testApplicationDoStreamStreamClient) Recv() (*Message, error) { m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *testApplicationClient) DoUnaryUnaryError(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) { out := new(Message) err := c.cc.Invoke(ctx, "/TestApplication/DoUnaryUnaryError", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *testApplicationClient) DoUnaryStreamError(ctx context.Context, in *Message, opts ...grpc.CallOption) (TestApplication_DoUnaryStreamErrorClient, error) { stream, err := c.cc.NewStream(ctx, &_TestApplication_serviceDesc.Streams[3], "/TestApplication/DoUnaryStreamError", opts...) if err != nil { return nil, err } x := &testApplicationDoUnaryStreamErrorClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type TestApplication_DoUnaryStreamErrorClient interface { Recv() (*Message, error) grpc.ClientStream } type testApplicationDoUnaryStreamErrorClient struct { grpc.ClientStream } func (x *testApplicationDoUnaryStreamErrorClient) Recv() (*Message, error) { m := new(Message) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // TestApplicationServer is the server API for TestApplication service. type TestApplicationServer interface { DoUnaryUnary(context.Context, *Message) (*Message, error) DoUnaryStream(*Message, TestApplication_DoUnaryStreamServer) error DoStreamUnary(TestApplication_DoStreamUnaryServer) error DoStreamStream(TestApplication_DoStreamStreamServer) error DoUnaryUnaryError(context.Context, *Message) (*Message, error) DoUnaryStreamError(*Message, TestApplication_DoUnaryStreamErrorServer) error } func RegisterTestApplicationServer(s *grpc.Server, srv TestApplicationServer) { s.RegisterService(&_TestApplication_serviceDesc, srv) } func _TestApplication_DoUnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Message) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TestApplicationServer).DoUnaryUnary(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/TestApplication/DoUnaryUnary", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TestApplicationServer).DoUnaryUnary(ctx, req.(*Message)) } return interceptor(ctx, in, info, handler) } func _TestApplication_DoUnaryStream_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(Message) if err := stream.RecvMsg(m); err != nil { return err } return srv.(TestApplicationServer).DoUnaryStream(m, &testApplicationDoUnaryStreamServer{stream}) } type TestApplication_DoUnaryStreamServer interface { Send(*Message) error grpc.ServerStream } type testApplicationDoUnaryStreamServer struct { grpc.ServerStream } func (x *testApplicationDoUnaryStreamServer) Send(m *Message) error { return x.ServerStream.SendMsg(m) } func _TestApplication_DoStreamUnary_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(TestApplicationServer).DoStreamUnary(&testApplicationDoStreamUnaryServer{stream}) } type TestApplication_DoStreamUnaryServer interface { SendAndClose(*Message) error Recv() (*Message, error) grpc.ServerStream } type testApplicationDoStreamUnaryServer struct { grpc.ServerStream } func (x *testApplicationDoStreamUnaryServer) SendAndClose(m *Message) error { return x.ServerStream.SendMsg(m) } func (x *testApplicationDoStreamUnaryServer) Recv() (*Message, error) { m := new(Message) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _TestApplication_DoStreamStream_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(TestApplicationServer).DoStreamStream(&testApplicationDoStreamStreamServer{stream}) } type TestApplication_DoStreamStreamServer interface { Send(*Message) error Recv() (*Message, error) grpc.ServerStream } type testApplicationDoStreamStreamServer struct { grpc.ServerStream } func (x *testApplicationDoStreamStreamServer) Send(m *Message) error { return x.ServerStream.SendMsg(m) } func (x *testApplicationDoStreamStreamServer) Recv() (*Message, error) { m := new(Message) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _TestApplication_DoUnaryUnaryError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Message) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TestApplicationServer).DoUnaryUnaryError(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/TestApplication/DoUnaryUnaryError", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TestApplicationServer).DoUnaryUnaryError(ctx, req.(*Message)) } return interceptor(ctx, in, info, handler) } func _TestApplication_DoUnaryStreamError_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(Message) if err := stream.RecvMsg(m); err != nil { return err } return srv.(TestApplicationServer).DoUnaryStreamError(m, &testApplicationDoUnaryStreamErrorServer{stream}) } type TestApplication_DoUnaryStreamErrorServer interface { Send(*Message) error grpc.ServerStream } type testApplicationDoUnaryStreamErrorServer struct { grpc.ServerStream } func (x *testApplicationDoUnaryStreamErrorServer) Send(m *Message) error { return x.ServerStream.SendMsg(m) } var _TestApplication_serviceDesc = grpc.ServiceDesc{ ServiceName: "TestApplication", HandlerType: (*TestApplicationServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "DoUnaryUnary", Handler: _TestApplication_DoUnaryUnary_Handler, }, { MethodName: "DoUnaryUnaryError", Handler: _TestApplication_DoUnaryUnaryError_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "DoUnaryStream", Handler: _TestApplication_DoUnaryStream_Handler, ServerStreams: true, }, { StreamName: "DoStreamUnary", Handler: _TestApplication_DoStreamUnary_Handler, ClientStreams: true, }, { StreamName: "DoStreamStream", Handler: _TestApplication_DoStreamStream_Handler, ServerStreams: true, ClientStreams: true, }, { StreamName: "DoUnaryStreamError", Handler: _TestApplication_DoUnaryStreamError_Handler, ServerStreams: true, }, }, Metadata: "testapp.proto", } go-agent-3.42.0/v3/integrations/nrgrpc/testapp/testapp.proto000066400000000000000000000010451510742411500240570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; service TestApplication { rpc DoUnaryUnary(Message) returns (Message) {} rpc DoUnaryStream(Message) returns (stream Message) {} rpc DoStreamUnary(stream Message) returns (Message) {} rpc DoStreamStream(stream Message) returns (stream Message) {} rpc DoUnaryUnaryError(Message) returns (Message) {} rpc DoUnaryStreamError(Message) returns (stream Message) {} } message Message { string text = 1; } go-agent-3.42.0/v3/integrations/nrhttprouter/000077500000000000000000000000001510742411500211175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrhttprouter/LICENSE.txt000066400000000000000000000264501510742411500227510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrhttprouter/README.md000066400000000000000000000007551510742411500224050ustar00rootroot00000000000000# v3/integrations/nrhttprouter [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrhttprouter?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrhttprouter) Package `nrhttprouter` instruments https://github.com/julienschmidt/httprouter applications. ```go import "github.com/newrelic/go-agent/v3/integrations/nrhttprouter" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrhttprouter). go-agent-3.42.0/v3/integrations/nrhttprouter/example/000077500000000000000000000000001510742411500225525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrhttprouter/example/main.go000066400000000000000000000017631510742411500240340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "net/http" "os" "github.com/julienschmidt/httprouter" "github.com/newrelic/go-agent/v3/integrations/nrhttprouter" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Write([]byte("welcome\n")) } func hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { w.Write([]byte(fmt.Sprintf("hello %s\n", ps.ByName("name")))) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("httprouter App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Use an *nrhttprouter.Router in place of an *httprouter.Router. router := nrhttprouter.New(app) router.GET("/", index) router.GET("/hello/:name", hello) http.ListenAndServe(":8000", router) } go-agent-3.42.0/v3/integrations/nrhttprouter/go.mod000066400000000000000000000006311510742411500222250ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrhttprouter // As of Dec 2019, the httprouter go.mod file uses 1.7: // https://github.com/julienschmidt/httprouter/blob/master/go.mod go 1.24 require ( // v1.3.0 is the earliest version of httprouter using modules. github.com/julienschmidt/httprouter v1.3.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrhttprouter/nrhttprouter.go000066400000000000000000000114261510742411500242320ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrhttprouter instruments https://github.com/julienschmidt/httprouter // applications. // // Use this package to instrument inbound requests handled by a // httprouter.Router. Use an *nrhttprouter.Router in place of your // *httprouter.Router. Example: // // package main // // import ( // "fmt" // "net/http" // "os" // // "github.com/julienschmidt/httprouter" // newrelic "github.com/newrelic/go-agent/v3/newrelic" // "github.com/newrelic/go-agent/v3/integrations/nrhttprouter" // ) // // func main() { // cfg := newrelic.NewConfig("httprouter App", os.Getenv("NEW_RELIC_LICENSE_KEY")) // app, _ := newrelic.NewApplication(cfg) // // // Create the Router replacement: // router := nrhttprouter.New(app) // // router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { // w.Write([]byte("welcome\n")) // }) // router.GET("/hello/:name", (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // w.Write([]byte(fmt.Sprintf("hello %s\n", ps.ByName("name")))) // }) // http.ListenAndServe(":8000", router) // } // // Runnable example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrhttprouter/example/main.go package nrhttprouter import ( "net/http" "github.com/julienschmidt/httprouter" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "framework", "httprouter") } // Router should be used in place of httprouter.Router. Create it using // New(). type Router struct { *httprouter.Router application *newrelic.Application } // New creates a new Router to be used in place of httprouter.Router. func New(app *newrelic.Application) *Router { return &Router{ Router: httprouter.New(), application: app, } } func txnName(method, path string) string { return method + " " + path } func (r *Router) handle(method string, path string, original httprouter.Handle) { handle := original if nil != r.application { handle = func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { txn := r.application.StartTransaction(txnName(method, path)) if newrelic.IsSecurityAgentPresent() { txn.SetCsecAttributes(newrelic.AttributeCsecRoute, path) } txn.SetWebRequestHTTP(req) w = txn.SetWebResponse(w) defer txn.End() req = newrelic.RequestWithTransactionContext(req, txn) original(w, req, ps) } } r.Router.Handle(method, path, handle) if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", path, method, internal.HandlerName(original)) } } // DELETE replaces httprouter.Router.DELETE. func (r *Router) DELETE(path string, h httprouter.Handle) { r.handle(http.MethodDelete, path, h) } // GET replaces httprouter.Router.GET. func (r *Router) GET(path string, h httprouter.Handle) { r.handle(http.MethodGet, path, h) } // HEAD replaces httprouter.Router.HEAD. func (r *Router) HEAD(path string, h httprouter.Handle) { r.handle(http.MethodHead, path, h) } // OPTIONS replaces httprouter.Router.OPTIONS. func (r *Router) OPTIONS(path string, h httprouter.Handle) { r.handle(http.MethodOptions, path, h) } // PATCH replaces httprouter.Router.PATCH. func (r *Router) PATCH(path string, h httprouter.Handle) { r.handle(http.MethodPatch, path, h) } // POST replaces httprouter.Router.POST. func (r *Router) POST(path string, h httprouter.Handle) { r.handle(http.MethodPost, path, h) } // PUT replaces httprouter.Router.PUT. func (r *Router) PUT(path string, h httprouter.Handle) { r.handle(http.MethodPut, path, h) } // Handle replaces httprouter.Router.Handle. func (r *Router) Handle(method, path string, h httprouter.Handle) { r.handle(method, path, h) } // Handler replaces httprouter.Router.Handler. func (r *Router) Handler(method, path string, handler http.Handler) { _, h := newrelic.WrapHandle(r.application, path, handler) r.Router.Handler(method, path, h) } // HandlerFunc replaces httprouter.Router.HandlerFunc. func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { r.Handler(method, path, handler) } // ServeHTTP replaces httprouter.Router.ServeHTTP. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { traceID := "" if nil != r.application { h, _, _ := r.Router.Lookup(req.Method, req.URL.Path) if nil == h { txn := r.application.StartTransaction("NotFound") defer txn.End() req = newrelic.RequestWithTransactionContext(req, txn) txn.SetWebRequestHTTP(req) w = txn.SetWebResponse(w) traceID = txn.GetLinkingMetadata().TraceID } } r.Router.ServeHTTP(w, req) if newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header(), traceID) } } go-agent-3.42.0/v3/integrations/nrhttprouter/nrhttprouter_test.go000066400000000000000000000222571510742411500252750ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrhttprouter import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/julienschmidt/httprouter" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestMethodFunctions(t *testing.T) { methodFuncs := []struct { Method string Fn func(*Router) func(string, httprouter.Handle) }{ {Method: "DELETE", Fn: func(r *Router) func(string, httprouter.Handle) { return r.DELETE }}, {Method: "GET", Fn: func(r *Router) func(string, httprouter.Handle) { return r.GET }}, {Method: "HEAD", Fn: func(r *Router) func(string, httprouter.Handle) { return r.HEAD }}, {Method: "OPTIONS", Fn: func(r *Router) func(string, httprouter.Handle) { return r.OPTIONS }}, {Method: "PATCH", Fn: func(r *Router) func(string, httprouter.Handle) { return r.PATCH }}, {Method: "POST", Fn: func(r *Router) func(string, httprouter.Handle) { return r.POST }}, {Method: "PUT", Fn: func(r *Router) func(string, httprouter.Handle) { return r.PUT }}, } for _, md := range methodFuncs { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) md.Fn(router)("/hello/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("hi %s", ps.ByName("name")))) }) response := httptest.NewRecorder() req, err := http.NewRequest(md.Method, "/hello/person", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi person" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: md.Method + " /hello/:name", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } } func TestGetNoApplication(t *testing.T) { router := New(nil) router.GET("/hello/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { w.Write([]byte(fmt.Sprintf("hi %s", ps.ByName("name")))) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/person", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi person" { t.Error("wrong response body", respBody) } } func TestHandle(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.Handle("GET", "/hello/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("hi %s", ps.ByName("name")))) txn := newrelic.FromContext(r.Context()) txn.AddAttribute("color", "purple") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/person", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi person" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello/:name", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello/:name", "nr.apdexPerfZone": internal.MatchAnything, "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, UserAttributes: map[string]interface{}{ "color": "purple", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 500, "http.statusCode": 500, "request.method": "GET", "request.uri": "/hello/person", }, }, }) } func TestHandler(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.Handler("GET", "/hello/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte("hi there")) txn := newrelic.FromContext(r.Context()) txn.AddAttribute("color", "purple") })) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi there" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello/", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/GET /hello/", "nr.apdexPerfZone": internal.MatchAnything, "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, UserAttributes: map[string]interface{}{ "color": "purple", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 500, "http.statusCode": 500, "request.method": "GET", "request.uri": "/hello/", }, }, }) } func TestHandlerMissingApplication(t *testing.T) { router := New(nil) router.Handler("GET", "/hello/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.Write([]byte("hi there")) })) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi there" { t.Error("wrong response body", respBody) } } func TestHandlerFunc(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.HandlerFunc("GET", "/hello/", func(w http.ResponseWriter, r *http.Request) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte("hi there")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "hi there" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "GET /hello/", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) } func TestNotFound(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte("not found!")) txn := newrelic.FromContext(r.Context()) txn.AddAttribute("color", "purple") }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "not found!" { t.Error("wrong response body", respBody) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "NotFound", IsWeb: true, NumErrors: 1, UnknownCaller: true, ErrorByCaller: true, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/NotFound", "nr.apdexPerfZone": internal.MatchAnything, "sampled": false, // Note: "*" is a wildcard value "guid": "*", "traceId": "*", "priority": "*", }, UserAttributes: map[string]interface{}{ "color": "purple", }, AgentAttributes: map[string]interface{}{ "httpResponseCode": 500, "http.statusCode": 500, "request.method": "GET", "request.uri": "/hello/", }, }, }) } func TestNotFoundMissingApplication(t *testing.T) { router := New(nil) router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Test that the Transaction is used as the response writer. w.WriteHeader(500) w.Write([]byte("not found!")) }) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if respBody := response.Body.String(); respBody != "not found!" { t.Error("wrong response body", respBody) } } func TestNotFoundNotSet(t *testing.T) { app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) response := httptest.NewRecorder() req, err := http.NewRequest("GET", "/hello/", nil) if err != nil { t.Fatal(err) } router.ServeHTTP(response, req) if response.Code != 404 { t.Error(response.Code) } app.ExpectTxnMetrics(t, internal.WantTxn{ Name: "NotFound", IsWeb: true, UnknownCaller: true, }) } go-agent-3.42.0/v3/integrations/nrlambda/000077500000000000000000000000001510742411500201175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlambda/LICENSE.txt000066400000000000000000000264501510742411500217510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrlambda/README.md000066400000000000000000000006541510742411500214030ustar00rootroot00000000000000# v3/integrations/nrlambda [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlambda?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlambda) Package `nrlambda` adds support for AWS Lambda. ```go import "github.com/newrelic/go-agent/v3/integrations/nrlambda" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlambda). go-agent-3.42.0/v3/integrations/nrlambda/config.go000066400000000000000000000025231510742411500217150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlambda import ( "os" "time" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) // ConfigOption populates a newrelic.Config with correct default settings for a // Lambda serverless environment. ConfigOption will populate fields based on // environment variables common to all New Relic agents that support Lambda. // Environment variables NEW_RELIC_ACCOUNT_ID, NEW_RELIC_TRUSTED_ACCOUNT_KEY, // and NEW_RELIC_PRIMARY_APPLICATION_ID configure fields required for // distributed tracing. Environment variable NEW_RELIC_APDEX_T may be used to // set a custom apdex threshold. func ConfigOption() newrelic.ConfigOption { return newConfigInternal(os.Getenv) } func newConfigInternal(getenv func(string) string) newrelic.ConfigOption { return func(cfg *newrelic.Config) { cfg.ServerlessMode.Enabled = true cfg.ServerlessMode.AccountID = getenv("NEW_RELIC_ACCOUNT_ID") cfg.ServerlessMode.TrustedAccountKey = getenv("NEW_RELIC_TRUSTED_ACCOUNT_KEY") cfg.ServerlessMode.PrimaryAppID = getenv("NEW_RELIC_PRIMARY_APPLICATION_ID") cfg.DistributedTracer.Enabled = true if s := getenv("NEW_RELIC_APDEX_T"); "" != s { if apdex, err := time.ParseDuration(s + "s"); nil == err { cfg.ServerlessMode.ApdexThreshold = apdex } } } } go-agent-3.42.0/v3/integrations/nrlambda/config_test.go000066400000000000000000000021151510742411500227510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlambda import ( "testing" "time" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestNewConfig(t *testing.T) { opt := newConfigInternal(func(key string) string { switch key { case "NEW_RELIC_ACCOUNT_ID": return "the-account-id" case "NEW_RELIC_TRUSTED_ACCOUNT_KEY": return "the-trust-key" case "NEW_RELIC_PRIMARY_APPLICATION_ID": return "the-app-id" case "NEW_RELIC_APDEX_T": return "2" default: return "" } }) cfg := &newrelic.Config{} opt(cfg) if !cfg.ServerlessMode.Enabled { t.Error(cfg.ServerlessMode.Enabled) } if cfg.ServerlessMode.AccountID != "the-account-id" { t.Error(cfg.ServerlessMode.AccountID) } if cfg.ServerlessMode.TrustedAccountKey != "the-trust-key" { t.Error(cfg.ServerlessMode.TrustedAccountKey) } if cfg.ServerlessMode.PrimaryAppID != "the-app-id" { t.Error(cfg.ServerlessMode.PrimaryAppID) } if cfg.ServerlessMode.ApdexThreshold != 2*time.Second { t.Error(cfg.ServerlessMode.ApdexThreshold) } } go-agent-3.42.0/v3/integrations/nrlambda/events.go000066400000000000000000000100531510742411500217510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlambda import ( "net/http" "net/url" "strings" "github.com/aws/aws-lambda-go/events" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func getEventSourceARN(event interface{}) string { switch v := event.(type) { case events.KinesisFirehoseEvent: return v.DeliveryStreamArn case *events.KinesisFirehoseEvent: return getEventSourceARN(safeDereference(v)) case events.KinesisEvent: if len(v.Records) > 0 { return v.Records[0].EventSourceArn } case *events.KinesisEvent: return getEventSourceARN(safeDereference(v)) case events.CodeCommitEvent: if len(v.Records) > 0 { return v.Records[0].EventSourceARN } case *events.CodeCommitEvent: return getEventSourceARN(safeDereference(v)) case events.DynamoDBEvent: if len(v.Records) > 0 { return v.Records[0].EventSourceArn } case *events.DynamoDBEvent: return getEventSourceARN(safeDereference(v)) case events.SQSEvent: if len(v.Records) > 0 { return v.Records[0].EventSourceARN } case *events.SQSEvent: return getEventSourceARN(safeDereference(v)) case events.S3Event: if len(v.Records) > 0 { return v.Records[0].S3.Bucket.Arn } case *events.S3Event: return getEventSourceARN(safeDereference(v)) case events.SNSEvent: if len(v.Records) > 0 { return v.Records[0].EventSubscriptionArn } case *events.SNSEvent: return getEventSourceARN(safeDereference(v)) } return "" } func eventWebRequest(event interface{}) *newrelic.WebRequest { var path string var request newrelic.WebRequest var headers map[string]string switch r := event.(type) { case events.APIGatewayProxyRequest: request.Method = r.HTTPMethod path = r.Path headers = r.Headers case *events.APIGatewayProxyRequest: return eventWebRequest(safeDereference(r)) case events.ALBTargetGroupRequest: request.Method = r.HTTPMethod path = r.Path headers = r.Headers case *events.ALBTargetGroupRequest: return eventWebRequest(safeDereference(r)) default: return nil } request.Header = make(http.Header, len(headers)) for k, v := range headers { request.Header.Set(k, v) } var host string if port := request.Header.Get("X-Forwarded-Port"); port != "" { host = ":" + port } request.URL = &url.URL{ Path: path, Host: host, } proto := strings.ToLower(request.Header.Get("X-Forwarded-Proto")) switch proto { case "https": request.Transport = newrelic.TransportHTTPS case "http": request.Transport = newrelic.TransportHTTP default: request.Transport = newrelic.TransportUnknown } return &request } func eventResponse(event interface{}) *response { var code int var headers map[string]string var multiValueHeaders map[string][]string switch r := event.(type) { case events.APIGatewayProxyResponse: code = r.StatusCode headers = r.Headers multiValueHeaders = r.MultiValueHeaders case *events.APIGatewayProxyResponse: return eventResponse(safeDereference(r)) case events.ALBTargetGroupResponse: code = r.StatusCode headers = r.Headers multiValueHeaders = r.MultiValueHeaders case *events.ALBTargetGroupResponse: return eventResponse(safeDereference(r)) default: return nil } // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format // "If you specify values for both headers and multiValueHeaders, API Gateway merges them into a single list. // If the same key-value pair is specified in both, only the values from multiValueHeaders will appear in the merged list." // // To match API Gateway's behavior, copy headers and then multiValueHeaders, so the latter takes priority for a given header key. hdr := make(http.Header, len(headers)+len(multiValueHeaders)) for k, v := range headers { hdr.Set(k, v) } for k, v := range multiValueHeaders { if len(v) == 0 { continue } hdr.Set(k, v[0]) } return &response{ code: code, header: hdr, } } func safeDereference[T any](p *T) T { if p == nil { var z T return z } return *p } go-agent-3.42.0/v3/integrations/nrlambda/events_test.go000066400000000000000000000256171510742411500230240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlambda import ( "net/http" "reflect" "testing" "github.com/aws/aws-lambda-go/events" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestGetEventAttributes(t *testing.T) { testcases := []struct { Name string Input interface{} Arn string }{ {Name: "nil", Input: nil, Arn: ""}, {Name: "SQSEvent empty", Input: events.SQSEvent{}, Arn: ""}, {Name: "SQSEvent", Input: events.SQSEvent{ Records: []events.SQSMessage{{ EventSourceARN: "ARN", }}, }, Arn: "ARN"}, {Name: "*SQSEvent nil", Input: (*events.SQSEvent)(nil), Arn: ""}, {Name: "*SQSEvent", Input: &events.SQSEvent{ Records: []events.SQSMessage{{ EventSourceARN: "ARN", }}, }, Arn: "ARN"}, {Name: "SNSEvent empty", Input: events.SNSEvent{}, Arn: ""}, {Name: "SNSEvent", Input: events.SNSEvent{ Records: []events.SNSEventRecord{{ EventSubscriptionArn: "ARN", }}, }, Arn: "ARN"}, {Name: "*SNSEvent nil", Input: (*events.SNSEvent)(nil), Arn: ""}, {Name: "*SNSEvent", Input: &events.SNSEvent{ Records: []events.SNSEventRecord{{ EventSubscriptionArn: "ARN", }}, }, Arn: "ARN"}, {Name: "S3Event empty", Input: events.S3Event{}, Arn: ""}, {Name: "S3Event", Input: events.S3Event{ Records: []events.S3EventRecord{{ S3: events.S3Entity{ Bucket: events.S3Bucket{ Arn: "ARN", }, }, }}, }, Arn: "ARN"}, {Name: "*S3Event nil", Input: (*events.S3Event)(nil), Arn: ""}, {Name: "*S3Event", Input: &events.S3Event{ Records: []events.S3EventRecord{{ S3: events.S3Entity{ Bucket: events.S3Bucket{ Arn: "ARN", }, }, }}, }, Arn: "ARN"}, {Name: "DynamoDBEvent empty", Input: events.DynamoDBEvent{}, Arn: ""}, {Name: "DynamoDBEvent", Input: events.DynamoDBEvent{ Records: []events.DynamoDBEventRecord{{ EventSourceArn: "ARN", }}, }, Arn: "ARN"}, {Name: "*DynamoDBEvent nil", Input: (*events.DynamoDBEvent)(nil), Arn: ""}, {Name: "*DynamoDBEvent", Input: &events.DynamoDBEvent{ Records: []events.DynamoDBEventRecord{{ EventSourceArn: "ARN", }}, }, Arn: "ARN"}, {Name: "CodeCommitEvent empty", Input: events.CodeCommitEvent{}, Arn: ""}, {Name: "CodeCommitEvent", Input: events.CodeCommitEvent{ Records: []events.CodeCommitRecord{{ EventSourceARN: "ARN", }}, }, Arn: "ARN"}, {Name: "*CodeCommitEvent nil", Input: (*events.CodeCommitEvent)(nil), Arn: ""}, {Name: "*CodeCommitEvent", Input: &events.CodeCommitEvent{ Records: []events.CodeCommitRecord{{ EventSourceARN: "ARN", }}, }, Arn: "ARN"}, {Name: "KinesisEvent empty", Input: events.KinesisEvent{}, Arn: ""}, {Name: "KinesisEvent", Input: events.KinesisEvent{ Records: []events.KinesisEventRecord{{ EventSourceArn: "ARN", }}, }, Arn: "ARN"}, {Name: "*KinesisEvent nil", Input: (*events.KinesisEvent)(nil), Arn: ""}, {Name: "*KinesisEvent", Input: &events.KinesisEvent{ Records: []events.KinesisEventRecord{{ EventSourceArn: "ARN", }}, }, Arn: "ARN"}, {Name: "KinesisFirehoseEvent empty", Input: events.KinesisFirehoseEvent{}, Arn: ""}, {Name: "KinesisFirehoseEvent", Input: events.KinesisFirehoseEvent{ DeliveryStreamArn: "ARN", }, Arn: "ARN"}, {Name: "*KinesisFirehoseEvent nil", Input: (*events.KinesisFirehoseEvent)(nil), Arn: ""}, {Name: "*KinesisFirehoseEvent", Input: &events.KinesisFirehoseEvent{ DeliveryStreamArn: "ARN", }, Arn: "ARN"}, } for _, testcase := range testcases { arn := getEventSourceARN(testcase.Input) if arn != testcase.Arn { t.Error(testcase.Name, arn, testcase.Arn) } } } func TestEventWebRequest(t *testing.T) { // First test a type that does not count as a web request. req := eventWebRequest(22) if nil != req { t.Error(req) } testcases := []struct { testname string input interface{} numHeaders int method string urlString string transport newrelic.TransportType }{ { testname: "empty APIGatewayProxyRequest", input: events.APIGatewayProxyRequest{}, numHeaders: 0, method: "", urlString: "", transport: newrelic.TransportUnknown, }, { testname: "populated APIGatewayProxyRequest", input: events.APIGatewayProxyRequest{ Headers: map[string]string{ "x-forwarded-port": "4000", "x-forwarded-proto": "HTTPS", }, HTTPMethod: "GET", Path: "the/path", }, numHeaders: 2, method: "GET", urlString: "//:4000/the/path", transport: newrelic.TransportHTTPS, }, { testname: "nil *APIGatewayProxyRequest", input: (*events.APIGatewayProxyRequest)(nil), numHeaders: 0, method: "", urlString: "", transport: newrelic.TransportUnknown, }, { testname: "populated *APIGatewayProxyRequest", input: &events.APIGatewayProxyRequest{ Headers: map[string]string{ "x-forwarded-port": "4000", "x-forwarded-proto": "HTTPS", }, HTTPMethod: "GET", Path: "the/path", }, numHeaders: 2, method: "GET", urlString: "//:4000/the/path", transport: newrelic.TransportHTTPS, }, { testname: "empty ALBTargetGroupRequest", input: events.ALBTargetGroupRequest{}, numHeaders: 0, method: "", urlString: "", transport: newrelic.TransportUnknown, }, { testname: "populated ALBTargetGroupRequest", input: events.ALBTargetGroupRequest{ Headers: map[string]string{ "x-forwarded-port": "3000", "x-forwarded-proto": "HttP", }, HTTPMethod: "GET", Path: "the/path", }, numHeaders: 2, method: "GET", urlString: "//:3000/the/path", transport: newrelic.TransportHTTP, }, { testname: "nil *ALBTargetGroupRequest", input: (*events.ALBTargetGroupRequest)(nil), numHeaders: 0, method: "", urlString: "", transport: newrelic.TransportUnknown, }, { testname: "populated *ALBTargetGroupRequest", input: &events.ALBTargetGroupRequest{ Headers: map[string]string{ "x-forwarded-port": "3000", "x-forwarded-proto": "HttP", }, HTTPMethod: "GET", Path: "the/path", }, numHeaders: 2, method: "GET", urlString: "//:3000/the/path", transport: newrelic.TransportHTTP, }, } for _, tc := range testcases { req = eventWebRequest(tc.input) if req == nil { t.Error(tc.testname, "no request returned") continue } if h := req.Header; len(h) != tc.numHeaders { t.Error(tc.testname, "header len mismatch", h, tc.numHeaders) } if u := req.URL.String(); u != tc.urlString { t.Error(tc.testname, "url mismatch", u, tc.urlString) } if m := req.Method; m != tc.method { t.Error(tc.testname, "method mismatch", m, tc.method) } if tr := req.Transport; tr != tc.transport { t.Error(tc.testname, "transport mismatch", tr, tc.transport) } } } func TestEventResponse(t *testing.T) { // First test a type that does not count as a web request. resp := eventResponse(22) if nil != resp { t.Error(resp) } runTest := func(t *testing.T, input any, want *response) { resp = eventResponse(input) if resp == nil { t.Fatal("no response returned") } if !reflect.DeepEqual(resp.header, want.header) { t.Error("header mismatch", resp.header, want.header) } if resp.code != want.code { t.Error("status code mismatch", resp.code, want.code) } } testcases := []struct { testname string headers map[string]string multiValueHeaders map[string][]string wantHeaders http.Header }{ { testname: "with Headers", headers: map[string]string{ "x-custom-header": "my custom header value", }, wantHeaders: http.Header{ "X-Custom-Header": {"my custom header value"}, }, }, { testname: "with MultiValueHeaders", multiValueHeaders: map[string][]string{ "x-custom-header": {"my custom header value", "another value"}, }, wantHeaders: http.Header{ "X-Custom-Header": {"my custom header value"}, }, }, { testname: "with Headers and MultiValueHeaders", headers: map[string]string{ "x-custom-header": "my custom header value", }, multiValueHeaders: map[string][]string{ "x-custom-header-2": {"my second custom header value"}, "empty-header": {}, }, wantHeaders: http.Header{ "X-Custom-Header": {"my custom header value"}, "X-Custom-Header-2": {"my second custom header value"}, }, }, { testname: "with overlapping Headers and MultiValueHeaders", headers: map[string]string{ "x-custom-header": "my custom header value", }, multiValueHeaders: map[string][]string{ "X-CUSTOM-HEADER": {"my second custom header value"}, }, wantHeaders: http.Header{ "X-Custom-Header": {"my second custom header value"}, }, }, } t.Run("APIGatewayProxyResponse", func(t *testing.T) { t.Run("empty", func(t *testing.T) { input := events.APIGatewayProxyResponse{} runTest(t, input, &response{ header: http.Header{}, code: 0, }) }) for _, tc := range testcases { t.Run(tc.testname, func(t *testing.T) { input := events.APIGatewayProxyResponse{ StatusCode: 200, Headers: tc.headers, MultiValueHeaders: tc.multiValueHeaders, } runTest(t, input, &response{ header: tc.wantHeaders, code: 200, }) }) } }) t.Run("*APIGatewayProxyResponse", func(t *testing.T) { t.Run("nil", func(t *testing.T) { input := (*events.APIGatewayProxyResponse)(nil) runTest(t, input, &response{ header: http.Header{}, code: 0, }) }) for _, tc := range testcases { t.Run(tc.testname, func(t *testing.T) { input := &events.APIGatewayProxyResponse{ StatusCode: 200, Headers: tc.headers, MultiValueHeaders: tc.multiValueHeaders, } runTest(t, input, &response{ header: tc.wantHeaders, code: 200, }) }) } }) t.Run("ALBTargetGroupResponse", func(t *testing.T) { t.Run("empty", func(t *testing.T) { input := events.ALBTargetGroupResponse{} runTest(t, input, &response{ header: http.Header{}, code: 0, }) }) for _, tc := range testcases { t.Run(tc.testname, func(t *testing.T) { input := events.ALBTargetGroupResponse{ StatusCode: 200, Headers: tc.headers, MultiValueHeaders: tc.multiValueHeaders, } runTest(t, input, &response{ header: tc.wantHeaders, code: 200, }) }) } }) t.Run("*ALBTargetGroupResponse", func(t *testing.T) { t.Run("nil", func(t *testing.T) { input := (*events.ALBTargetGroupResponse)(nil) runTest(t, input, &response{ header: http.Header{}, code: 0, }) }) for _, tc := range testcases { t.Run(tc.testname, func(t *testing.T) { input := &events.ALBTargetGroupResponse{ StatusCode: 200, Headers: tc.headers, MultiValueHeaders: tc.multiValueHeaders, } runTest(t, input, &response{ header: tc.wantHeaders, code: 200, }) }) } }) } go-agent-3.42.0/v3/integrations/nrlambda/example/000077500000000000000000000000001510742411500215525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlambda/example/main.go000066400000000000000000000021531510742411500230260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "github.com/newrelic/go-agent/v3/integrations/nrlambda" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func handler(ctx context.Context) { // The nrlambda handler instrumentation will add the transaction to the // context. Access it using newrelic.FromContext to add additional // instrumentation. txn := newrelic.FromContext(ctx) txn.AddAttribute("userLevel", "gold") txn.Application().RecordCustomEvent("MyEvent", map[string]interface{}{ "zip": "zap", }) fmt.Println("hello world") } func main() { // Pass nrlambda.ConfigOption() into newrelic.NewApplication to set // Lambda specific configuration settings including // Config.ServerlessMode.Enabled. app, err := newrelic.NewApplication(nrlambda.ConfigOption()) if nil != err { fmt.Println("error creating app (invalid config):", err) } // nrlambda.Start should be used in place of lambda.Start. // nrlambda.StartHandler should be used in place of lambda.StartHandler. nrlambda.Start(handler, app) } go-agent-3.42.0/v3/integrations/nrlambda/go.mod000066400000000000000000000003251510742411500212250ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrlambda go 1.24 require ( github.com/aws/aws-lambda-go v1.41.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrlambda/handler.go000066400000000000000000000130011510742411500220560ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrlambda adds support for AWS Lambda. // // Use this package to instrument your AWS Lambda handler function. Data is // sent to CloudWatch when the Lambda is invoked. CloudWatch collects Lambda // log data and sends it to a New Relic log-ingestion Lambda. The log-ingestion // Lambda sends that data to us. // // Monitoring AWS Lambda requires several steps shown here: // https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/enable-new-relic-monitoring-aws-lambda // // Example: https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrlambda/example/main.go package nrlambda import ( "context" "io" "net/http" "os" "sync" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-lambda-go/lambda/handlertrace" "github.com/aws/aws-lambda-go/lambdacontext" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) type response struct { header http.Header code int } var _ http.ResponseWriter = &response{} func (r *response) Header() http.Header { return r.header } func (r *response) Write([]byte) (int, error) { return 0, nil } func (r *response) WriteHeader(int) {} func requestEvent(ctx context.Context, event interface{}) { txn := newrelic.FromContext(ctx) if nil == txn { return } if sourceARN := getEventSourceARN(event); "" != sourceARN { integrationsupport.AddAgentAttribute(txn, newrelic.AttributeAWSLambdaEventSourceARN, sourceARN, nil) } if request := eventWebRequest(event); nil != request { txn.SetWebRequest(*request) } } func responseEvent(ctx context.Context, event interface{}) { txn := newrelic.FromContext(ctx) if nil == txn { return } if rw := eventResponse(event); nil != rw && 0 != rw.code { w := txn.SetWebResponse(rw) w.WriteHeader(rw.code) } } type writerProvider interface { borrowWriter(needsWriter func(writer io.Writer)) } type defaultWriterProvider struct { } const telemetryNamedPipe = "/tmp/newrelic-telemetry" func (wp *defaultWriterProvider) borrowWriter(needsWriter func(io.Writer)) { // If the telemetry named pipe exists and is writable, use it instead of stdout pipeFile, err := os.OpenFile(telemetryNamedPipe, os.O_WRONLY, 0) if err != nil { needsWriter(os.Stdout) return } // We need to close the pipe; of course we don't close stdout defer pipeFile.Close() needsWriter(pipeFile) } func (h *wrappedHandler) Invoke(ctx context.Context, payload []byte) ([]byte, error) { var arn, requestID string if lctx, ok := lambdacontext.FromContext(ctx); ok { arn = lctx.InvokedFunctionArn requestID = lctx.AwsRequestID } defer h.hasWriter.borrowWriter(func(writer io.Writer) { internal.ServerlessWrite(h.app.Private, arn, writer) }) txn := h.app.StartTransaction(h.functionName) defer txn.End() integrationsupport.AddAgentAttribute(txn, newrelic.AttributeAWSRequestID, requestID, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeAWSLambdaARN, arn, nil) h.firstTransaction.Do(func() { integrationsupport.AddAgentAttribute(txn, newrelic.AttributeAWSLambdaColdStart, "", true) }) ctx = newrelic.NewContext(ctx, txn) ctx = handlertrace.NewContext(ctx, handlertrace.HandlerTrace{ RequestEvent: requestEvent, ResponseEvent: responseEvent, }) response, err := h.original.Invoke(ctx, payload) if nil != err { txn.NoticeError(err) } return response, err } type wrappedHandler struct { original lambda.Handler app *newrelic.Application // functionName is copied from lambdacontext.FunctionName for // deterministic tests that don't depend on environment variables. functionName string // Although we are told that each Lambda will only handle one request at // a time, we use a synchronization primitive to determine if this is // the first transaction for defensiveness in case of future changes. firstTransaction sync.Once // hasWriter is used to log the data JSON at the end of each transaction. // The writerProvider manages the lifecycle of the file handle being written // to, similar to the Loan pattern. This field exists mostly for testing. hasWriter writerProvider } // WrapHandler wraps the provided handler and returns a new handler with // instrumentation. StartHandler should generally be used in place of // WrapHandler: this function is exposed for consumers who are chaining // middlewares. func WrapHandler(handler lambda.Handler, app *newrelic.Application) lambda.Handler { if nil == app { return handler } return &wrappedHandler{ original: handler, app: app, functionName: lambdacontext.FunctionName, hasWriter: &defaultWriterProvider{}, } } // Wrap wraps the provided handler and returns a new handler with // instrumentation. Start should generally be used in place of Wrap. func Wrap(handler interface{}, app *newrelic.Application) lambda.Handler { return WrapHandler(lambda.NewHandler(handler), app) } // Start should be used in place of lambda.Start. Replace: // // lambda.Start(myhandler) // // With: // // nrlambda.Start(myhandler, app) func Start(handler interface{}, app *newrelic.Application) { lambda.StartHandler(Wrap(handler, app)) } // StartHandler should be used in place of lambda.StartHandler. Replace: // // lambda.StartHandler(myhandler) // // With: // // nrlambda.StartHandler(myhandler, app) func StartHandler(handler lambda.Handler, app *newrelic.Application) { lambda.StartHandler(WrapHandler(handler, app)) } go-agent-3.42.0/v3/integrations/nrlambda/handler_test.go000066400000000000000000000474651510742411500231420ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlambda import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "os" "strings" "testing" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambdacontext" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func testApp(getenv func(string) string, t *testing.T) *newrelic.Application { if nil == getenv { getenv = func(string) string { return "" } } cfg := newConfigInternal(getenv) app, err := newrelic.NewApplication(cfg, newrelic.ConfigCodeLevelMetricsEnabled(false)) if nil != err { t.Fatal(err) } internal.HarvestTesting(app.Private, nil) return app } func distributedTracingEnabled(key string) string { switch key { case "NEW_RELIC_ACCOUNT_ID": return "1" case "NEW_RELIC_TRUSTED_ACCOUNT_KEY": return "1" case "NEW_RELIC_PRIMARY_APPLICATION_ID": return "1" default: return "" } } // bufWriterProvider is a testing implementation of writerProvider type bufWriterProvider struct { buf io.Writer } func (bw bufWriterProvider) borrowWriter(needsWriter func(writer io.Writer)) { needsWriter(bw.buf) } func TestColdStart(t *testing.T) { originalHandler := func(c context.Context) {} app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} ctx := context.Background() lctx := &lambdacontext.LambdaContext{ AwsRequestID: "request-id", InvokedFunctionArn: "function-arn", } ctx = lambdacontext.NewContext(ctx, lctx) resp, err := wrapped.Invoke(ctx, nil) if nil != err || string(resp) != "null" { t.Error("unexpected response", err, string(resp)) } app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.requestId": "request-id", "aws.lambda.arn": "function-arn", "aws.lambda.coldStart": true, }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "transaction.name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.requestId": "request-id", "aws.lambda.arn": "function-arn", "aws.lambda.coldStart": true, }, }}) if 0 == buf.Len() { t.Error("no output written") } // Invoke the handler again to test the cold-start attribute absence. buf = &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} internal.HarvestTesting(app.Private, nil) resp, err = wrapped.Invoke(ctx, nil) if nil != err || string(resp) != "null" { t.Error("unexpected response", err, string(resp)) } app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.requestId": "request-id", "aws.lambda.arn": "function-arn", }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "transaction.name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.requestId": "request-id", "aws.lambda.arn": "function-arn", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestErrorCapture(t *testing.T) { returnError := errors.New("problem") originalHandler := func() error { return returnError } app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} resp, err := wrapped.Invoke(context.Background(), nil) if err != returnError || string(resp) != "" { t.Error(err, string(resp)) } app.Private.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, // Error metrics test the error capture. {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "transaction.name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "error.class": "*errors.errorString", "error.message": "problem", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestWrapNilApp(t *testing.T) { originalHandler := func() (int, error) { return 123, nil } wrapped := Wrap(originalHandler, nil) ctx := context.Background() resp, err := wrapped.Invoke(ctx, nil) if nil != err || string(resp) != "123" { t.Error("unexpected response", err, string(resp)) } } func TestSetWebRequest(t *testing.T) { originalHandler := func(events.APIGatewayProxyRequest) {} app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} req := events.APIGatewayProxyRequest{ Headers: map[string]string{ "X-Forwarded-Port": "4000", "X-Forwarded-Proto": "HTTPS", }, } reqbytes, err := json.Marshal(req) if err != nil { t.Error("unable to marshal json", err) } resp, err := wrapped.Invoke(context.Background(), reqbytes) if err != nil { t.Error(err, string(resp)) } app.Private.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/functionName", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, }) app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/functionName", "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "request.uri": "//:4000", }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/functionName", "transaction.name": "WebTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "request.uri": "//:4000", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestDistributedTracing(t *testing.T) { originalHandler := func(events.APIGatewayProxyRequest) {} app := testApp(distributedTracingEnabled, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} dtHdr := http.Header{} app.StartTransaction("hello").InsertDistributedTraceHeaders(dtHdr) hdr := map[string]string{ "X-Forwarded-Port": "4000", "X-Forwarded-Proto": "HTTPS", } for k := range dtHdr { if v := dtHdr.Get(k); v != "" { hdr[k] = v } } req := events.APIGatewayProxyRequest{Headers: hdr} reqbytes, err := json.Marshal(req) if err != nil { t.Error("unable to marshal json", err) } resp, err := wrapped.Invoke(context.Background(), reqbytes) if err != nil { t.Error(err, string(resp)) } app.Private.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/functionName", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/1/1/HTTPS/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/1/1/HTTPS/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/1/1/HTTPS/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/1/1/HTTPS/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, }) app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/functionName", "nr.apdexPerfZone": "S", "parent.account": "1", "parent.app": "1", "parent.transportType": "HTTPS", "parent.type": "App", "guid": internal.MatchAnything, "parent.transportDuration": internal.MatchAnything, "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "request.uri": "//:4000", }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/functionName", "transaction.name": "WebTransaction/Go/functionName", "guid": internal.MatchAnything, "parentId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "parent.account": "1", "parent.app": "1", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTPS", "parent.type": "App", "request.uri": "//:4000", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestEventARN(t *testing.T) { originalHandler := func(events.DynamoDBEvent) {} app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} req := events.DynamoDBEvent{ Records: []events.DynamoDBEventRecord{{ EventSourceArn: "ARN", }}, } reqbytes, err := json.Marshal(req) if err != nil { t.Error("unable to marshal json", err) } resp, err := wrapped.Invoke(context.Background(), reqbytes) if err != nil { t.Error(err, string(resp)) } app.Private.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "aws.lambda.eventSource.arn": "ARN", }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "transaction.name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.entryPoint": true, "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "aws.lambda.eventSource.arn": "ARN", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestAPIGatewayProxyResponse(t *testing.T) { originalHandler := func() (events.APIGatewayProxyResponse, error) { return events.APIGatewayProxyResponse{ Body: "Hello World", StatusCode: 200, Headers: map[string]string{ "Content-Type": "text/html", }, }, nil } app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} resp, err := wrapped.Invoke(context.Background(), nil) if nil != err { t.Error("unexpected err", err) } if !strings.Contains(string(resp), "Hello World") { t.Error("unexpected response", string(resp)) } app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "httpResponseCode": "200", "http.statusCode": "200", "response.headers.contentType": "text/html", }, }}) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/functionName", "transaction.name": "OtherTransaction/Go/functionName", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.lambda.coldStart": true, "httpResponseCode": "200", "http.statusCode": 200, "response.headers.contentType": "text/html", }, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestCustomEvent(t *testing.T) { originalHandler := func(c context.Context) { txn := newrelic.FromContext(c) txn.Application().RecordCustomEvent("myEvent", map[string]interface{}{ "zip": "zap", }) } app := testApp(nil, t) wrapped := Wrap(originalHandler, app) w := wrapped.(*wrappedHandler) w.functionName = "functionName" buf := &bytes.Buffer{} w.hasWriter = bufWriterProvider{buf} resp, err := wrapped.Invoke(context.Background(), nil) if nil != err || string(resp) != "null" { t.Error("unexpected response", err, string(resp)) } app.Private.(internal.Expect).ExpectCustomEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "myEvent", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "zip": "zap", }, AgentAttributes: map[string]interface{}{}, }}) if 0 == buf.Len() { t.Error("no output written") } } func TestDefaultWriterProvider(t *testing.T) { dwp := defaultWriterProvider{} dwp.borrowWriter(func(writer io.Writer) { if writer != os.Stdout { t.Error("Expected stdout") } }) const telemetryFile = "/tmp/newrelic-telemetry" defer os.Remove(telemetryFile) file, err := os.Create(telemetryFile) if err != nil { t.Error("Unexpected error creating telemetry file", err) } err = file.Close() if err != nil { t.Error("Error closing telemetry file", err) } dwp.borrowWriter(func(writer io.Writer) { if writer == os.Stdout { t.Error("Expected telemetry file, got stdout") } }) } go-agent-3.42.0/v3/integrations/nrlogrus/000077500000000000000000000000001510742411500202125ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlogrus/LICENSE.txt000066400000000000000000000264501510742411500220440ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrlogrus/README.md000066400000000000000000000007221510742411500214720ustar00rootroot00000000000000# v3/integrations/nrlogrus [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus) Package `nrlogrus` sends go-agent log messages to https://github.com/sirupsen/logrus. ```go import "github.com/newrelic/go-agent/v3/integrations/nrlogrus" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus). go-agent-3.42.0/v3/integrations/nrlogrus/examples/000077500000000000000000000000001510742411500220305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlogrus/examples/server-http-logs-in-context/000077500000000000000000000000001510742411500273435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go000066400000000000000000000044471510742411500306270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // An application that illustrates Distributed Tracing with Logs-in-Context // when using http.Server or similar frameworks. package main import ( "context" "fmt" "io" "net/http" "os" "time" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) type handler struct { App *newrelic.Application } func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { // The call to StartTransaction must include the response writer and the // request. txn := h.App.StartTransaction("server-txn") defer txn.End() txnLogger := logrus.WithContext(newrelic.NewContext(context.Background(), txn)) writer = txn.SetWebResponse(writer) txn.SetWebRequestHTTP(req) if req.URL.String() == "/segments" { defer txn.StartSegment("f1").End() txnLogger.Infof("/segments just started") func() { defer txn.StartSegment("f2").End() io.WriteString(writer, "segments!") time.Sleep(10 * time.Millisecond) txnLogger.Infof("segment func just about to complete") }() time.Sleep(10 * time.Millisecond) } else { // Transaction.WriteHeader has to be used instead of invoking // WriteHeader on the response writer. writer.WriteHeader(http.StatusNotFound) } txnLogger.Infof("handler completing") } func makeApplication() (*newrelic.Application, error) { app, err := newrelic.NewApplication( newrelic.ConfigAppName("HTTP Server App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), ) if nil != err { return nil, err } nrlogrusFormatter := nrlogrus.NewFormatter(app, &logrus.TextFormatter{}) logrus.SetFormatter(nrlogrusFormatter) // Alternatively and if preferred, create a new logger and use that logger // for logging with // log := logrus.New() // log.SetFormatter(nrlogrusFormatter) // Wait for the application to connect. if err = app.WaitForConnection(5 * time.Second); nil != err { return nil, err } return app, nil } func main() { app, err := makeApplication() if nil != err { fmt.Println(err) os.Exit(1) } logrus.Infof("Application Starting") server := http.Server{ Addr: ":8000", Handler: &handler{App: app}, } server.ListenAndServe() } go-agent-3.42.0/v3/integrations/nrlogrus/examples/server/000077500000000000000000000000001510742411500233365ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlogrus/examples/server/main.go000066400000000000000000000013671510742411500246200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "io" "net/http" "os" "github.com/newrelic/go-agent/v3/integrations/nrlogrus" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func main() { logrus.SetLevel(logrus.DebugLevel) app, err := newrelic.NewApplication( newrelic.ConfigAppName("Logrus App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), nrlogrus.ConfigStandardLogger(), ) if nil != err { fmt.Println(err) os.Exit(1) } http.HandleFunc(newrelic.WrapHandleFunc(app, "/", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello world") })) http.ListenAndServe(":8000", nil) } go-agent-3.42.0/v3/integrations/nrlogrus/go.mod000066400000000000000000000011451510742411500213210ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrlogrus // As of Dec 2019, the logrus go.mod file uses 1.13: // https://github.com/sirupsen/logrus/blob/master/go.mod go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus v1.0.0 // v1.1.0 is required for the Logger.GetLevel method, and is the earliest // version of logrus using modules. github.com/sirupsen/logrus v1.8.1 ) replace github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus => ../logcontext-v2/nrlogrus replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrlogrus/nrlogrus.go000066400000000000000000000046611510742411500224230ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrlogrus sends go-agent log messages to // https://github.com/sirupsen/logrus. // // Use this package if you are using logrus in your application and would like // the go-agent log messages to end up in the same place. If you are using // the logrus standard logger, use ConfigStandardLogger when creating your // application: // // app, err := newrelic.NewApplication( // newrelic.ConfigFromEnvironment(), // nrlogrus.ConfigStandardLogger(), // ) // // If you are using a particular logrus Logger instance, then use ConfigLogger: // // l := logrus.New() // l.SetLevel(logrus.DebugLevel) // app, err := newrelic.NewApplication( // newrelic.ConfigFromEnvironment(), // nrlogrus.ConfigLogger(l), // ) // // This package requires logrus version v1.1.0 and above. package nrlogrus import ( "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" ) func init() { internal.TrackUsage("integration", "logging", "logrus") } type shim struct { e *logrus.Entry l *logrus.Logger } func (s *shim) Error(msg string, c map[string]interface{}) { s.e.WithFields(c).Error(msg) } func (s *shim) Warn(msg string, c map[string]interface{}) { s.e.WithFields(c).Warn(msg) } func (s *shim) Info(msg string, c map[string]interface{}) { s.e.WithFields(c).Info(msg) } func (s *shim) Debug(msg string, c map[string]interface{}) { s.e.WithFields(c).Debug(msg) } func (s *shim) DebugEnabled() bool { lvl := s.l.GetLevel() return lvl >= logrus.DebugLevel } // StandardLogger returns a newrelic.Logger which forwards agent log messages to // the logrus package-level exported logger. func StandardLogger() newrelic.Logger { return Transform(logrus.StandardLogger()) } // Transform turns a *logrus.Logger into a newrelic.Logger. func Transform(l *logrus.Logger) newrelic.Logger { return &shim{ l: l, e: l.WithFields(logrus.Fields{ "component": "newrelic", }), } } // ConfigLogger configures the newrelic.Application to send log messsages to the // provided logrus logger. func ConfigLogger(l *logrus.Logger) newrelic.ConfigOption { return newrelic.ConfigLogger(Transform(l)) } // ConfigStandardLogger configures the newrelic.Application to send log // messsages to the standard logrus logger. func ConfigStandardLogger() newrelic.ConfigOption { return newrelic.ConfigLogger(StandardLogger()) } go-agent-3.42.0/v3/integrations/nrlogrus/nrlogrus_test.go000066400000000000000000000056411510742411500234610ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlogrus import ( "bytes" "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" "strings" "testing" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func bufferToStringAndReset(buf *bytes.Buffer) string { s := buf.String() buf.Reset() return s } func createLoggerWithBuffer(level logrus.Level) (*logrus.Logger, *bytes.Buffer) { buf := &bytes.Buffer{} l := logrus.New() l.SetOutput(buf) l.SetLevel(logrus.DebugLevel) return l, buf } func TestLogrusDebug(t *testing.T) { l, buf := createLoggerWithBuffer(logrus.DebugLevel) lg := Transform(l) lg.Debug("elephant", map[string]interface{}{"color": "gray"}) s := bufferToStringAndReset(buf) // check to see if the level is set to debug if !strings.Contains(s, "level=debug") { t.Error(s) } if !strings.Contains(s, "elephant") || !strings.Contains(s, "gray") { t.Error(s) } if enabled := lg.DebugEnabled(); !enabled { t.Error(enabled) } } func TestLogrusInfo(t *testing.T) { l, buf := createLoggerWithBuffer(logrus.InfoLevel) lg := Transform(l) lg.Info("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to info if !strings.Contains(s, "level=info") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestLogrusError(t *testing.T) { l, buf := createLoggerWithBuffer(logrus.ErrorLevel) lg := Transform(l) lg.Error("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to error if !strings.Contains(s, "level=error") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestLogrusWarn(t *testing.T) { l, buf := createLoggerWithBuffer(logrus.WarnLevel) lg := Transform(l) lg.Warn("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to warning if !strings.Contains(s, "level=warn") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestConfigLogger(t *testing.T) { l, buf := createLoggerWithBuffer(logrus.InfoLevel) integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, ConfigLogger(l), newrelic.ConfigAppLogForwardingEnabled(true), ) s := bufferToStringAndReset(buf) if !strings.Contains(s, "application created") || !strings.Contains(s, "my app") { t.Error(s) } } func TestConfigStandardLogger(t *testing.T) { buf := &bytes.Buffer{} integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, ConfigStandardLogger(), newrelic.ConfigDebugLogger(buf), ) s := bufferToStringAndReset(buf) if !strings.Contains(s, "application created") || !strings.Contains(s, "my app") { t.Error(s) } } go-agent-3.42.0/v3/integrations/nrlogxi/000077500000000000000000000000001510742411500200215ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrlogxi/LICENSE.txt000066400000000000000000000264501510742411500216530ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrlogxi/README.md000066400000000000000000000006621510742411500213040ustar00rootroot00000000000000# v3/integrations/nrlogxi [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi) Package `nrlogxi` supports https://github.com/mgutz/logxi. ```go import "github.com/newrelic/go-agent/v3/integrations/nrlogxi" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi). go-agent-3.42.0/v3/integrations/nrlogxi/example_test.go000066400000000000000000000011321510742411500230370ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlogxi_test import ( log "github.com/mgutz/logxi/v1" nrlogxi "github.com/newrelic/go-agent/v3/integrations/nrlogxi" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func Example() { // Create a new logxi logger: l := log.New("newrelic") l.SetLevel(log.LevelInfo) newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEWRELIC_LICENSE_KEY__"), // Use nrlogxi to register the logger with the agent: nrlogxi.ConfigLogger(l), ) } go-agent-3.42.0/v3/integrations/nrlogxi/go.mod000066400000000000000000000005751510742411500211360ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrlogxi // As of Dec 2019, logxi requires 1.3+: // https://github.com/mgutz/logxi#requirements go 1.24 require ( // 'v1', at commit aebf8a7d67ab, is the only logxi release. github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrlogxi/nrlogxi.go000066400000000000000000000031651510742411500220370ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrlogxi supports https://github.com/mgutz/logxi. // // Wrap your logxi Logger using nrlogxi.New to send agent log messages through // logxi. package nrlogxi import ( "math" log "github.com/mgutz/logxi/v1" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "logging", "logxi", "v1") } type shim struct { e log.Logger } func (l *shim) Error(msg string, context map[string]interface{}) { l.e.Error(msg, convert(context)...) } func (l *shim) Warn(msg string, context map[string]interface{}) { l.e.Warn(msg, convert(context)...) } func (l *shim) Info(msg string, context map[string]interface{}) { l.e.Info(msg, convert(context)...) } func (l *shim) Debug(msg string, context map[string]interface{}) { l.e.Debug(msg, convert(context)...) } func (l *shim) DebugEnabled() bool { return l.e.IsDebug() } func convert(c map[string]interface{}) []interface{} { if len(c) >= math.MaxInt32/2 { output := make([]interface{}, 0) return output } output := make([]interface{}, 0, 2*len(c)) for k, v := range c { output = append(output, k, v) } return output } // New returns a newrelic.Logger which forwards agent log messages to the // provided logxi Logger. func New(l log.Logger) newrelic.Logger { return &shim{ e: l, } } // ConfigLogger configures the newrelic.Application to send log messsages to the // provided logxi logger. func ConfigLogger(l log.Logger) newrelic.ConfigOption { return newrelic.ConfigLogger(New(l)) } go-agent-3.42.0/v3/integrations/nrlogxi/nrlogxi_test.go000066400000000000000000000050311510742411500230700ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrlogxi_test import ( "bytes" "strings" "testing" log "github.com/mgutz/logxi/v1" nrlogxi "github.com/newrelic/go-agent/v3/integrations/nrlogxi" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func bufferToStringAndReset(buf *bytes.Buffer) string { s := buf.String() buf.Reset() return s } func createLoggerWithBuffer() (newrelic.Logger, *bytes.Buffer) { buf := &bytes.Buffer{} l := log.NewLogger(buf, "LoggerName") l.SetLevel(log.LevelDebug) logger := nrlogxi.New(l) return logger, buf } func TestLogxiDebug(t *testing.T) { l, buf := createLoggerWithBuffer() l.Debug("elephant", map[string]interface{}{"color": "gray"}) s := bufferToStringAndReset(buf) // check to see if the level is set to debug if !l.DebugEnabled() { t.Error("Debug mode not enabled") } if !strings.Contains(s, "DBG") { t.Error(s) } if !strings.Contains(s, "elephant") || !strings.Contains(s, "gray") { t.Error(s) } } func TestLogxiInfo(t *testing.T) { l, buf := createLoggerWithBuffer() l.Info("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to info if !strings.Contains(s, "INF") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestLogxiError(t *testing.T) { l, buf := createLoggerWithBuffer() l.Error("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to error if !strings.Contains(s, "ERR") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestLogxiWarn(t *testing.T) { l, buf := createLoggerWithBuffer() l.Warn("tiger", map[string]interface{}{"color": "orange"}) s := bufferToStringAndReset(buf) // check to see if the level is set to warning if !strings.Contains(s, "WRN") { t.Error(s) } if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { t.Error(s) } } func TestConfigLogger(t *testing.T) { buf := &bytes.Buffer{} l := log.NewLogger(buf, "LoggerName") l.SetLevel(log.LevelDebug) integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, nrlogxi.ConfigLogger(l), newrelic.ConfigAppLogForwardingEnabled(true), ) s := bufferToStringAndReset(buf) if !strings.Contains(s, "application created") || !strings.Contains(s, "my app") { t.Error(s) } } go-agent-3.42.0/v3/integrations/nrmicro/000077500000000000000000000000001510742411500200105ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/LICENSE.txt000066400000000000000000000264501510742411500216420ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrmicro/README.md000066400000000000000000000006701510742411500212720ustar00rootroot00000000000000# v3/integrations/nrmicro [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmicro?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmicro) Package `nrmicro` instruments https://github.com/micro/go-micro. ```go import "github.com/newrelic/go-agent/v3/integrations/nrmicro" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmicro). go-agent-3.42.0/v3/integrations/nrmicro/example/000077500000000000000000000000001510742411500214435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/example/README.md000066400000000000000000000011731510742411500227240ustar00rootroot00000000000000# Example Go Micro apps In this directory you will find several example Go Micro apps that are instrumented using the New Relic agent. All of the apps assume that your New Relic license key is available as an environment variable named `NEW_RELIC_LICENSE_KEY` They can be run the standard way: * The sample Pub/Sub app: `go run pubsub/main.go` instruments both a publish and a subscribe method * The sample Server app: `go run server/server.go` instruments a handler method * The sample Client app: `go run client/client.go` instruments the client. * Note that in order for this to function, the server app must also be running. go-agent-3.42.0/v3/integrations/nrmicro/example/client/000077500000000000000000000000001510742411500227215ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/example/client/client.go000066400000000000000000000023401510742411500245250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "os" "time" "github.com/micro/go-micro" "github.com/newrelic/go-agent/v3/integrations/nrmicro" proto "github.com/newrelic/go-agent/v3/integrations/nrmicro/example/proto" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Micro Client"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } err = app.WaitForConnection(10 * time.Second) if nil != err { panic(err) } defer app.Shutdown(10 * time.Second) txn := app.StartTransaction("client") defer txn.End() service := micro.NewService( // Add the New Relic wrapper to the client which will create External // segments for each out going call. micro.WrapClient(nrmicro.ClientWrapper()), ) service.Init() ctx := newrelic.NewContext(context.Background(), txn) c := proto.NewGreeterService("greeter", service.Client()) rsp, err := c.Hello(ctx, &proto.HelloRequest{ Name: "John", }) if err != nil { fmt.Println(err) return } fmt.Println(rsp.Greeting) } go-agent-3.42.0/v3/integrations/nrmicro/example/proto/000077500000000000000000000000001510742411500226065ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/example/proto/greeter.micro.go000066400000000000000000000045061510742411500257070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-micro. DO NOT EDIT. // source: greeter.proto package greeter import ( fmt "fmt" proto "github.com/golang/protobuf/proto" math "math" ) import ( context "context" client "github.com/micro/go-micro/client" server "github.com/micro/go-micro/server" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ client.Option var _ server.Option // Client API for Greeter service type GreeterService interface { Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error) } type greeterService struct { c client.Client name string } func NewGreeterService(name string, c client.Client) GreeterService { if c == nil { c = client.NewClient() } if len(name) == 0 { name = "greeter" } return &greeterService{ c: c, name: name, } } func (c *greeterService) Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error) { req := c.c.NewRequest(c.name, "Greeter.Hello", in) out := new(HelloResponse) err := c.c.Call(ctx, req, out, opts...) if err != nil { return nil, err } return out, nil } // Server API for Greeter service type GreeterHandler interface { Hello(context.Context, *HelloRequest, *HelloResponse) error } func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error { type greeter interface { Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error } type Greeter struct { greeter } h := &greeterHandler{hdlr} return s.Handle(s.NewHandler(&Greeter{h}, opts...)) } type greeterHandler struct { GreeterHandler } func (h *greeterHandler) Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error { return h.GreeterHandler.Hello(ctx, in, out) } go-agent-3.42.0/v3/integrations/nrmicro/example/proto/greeter.pb.go000066400000000000000000000103511510742411500251720ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // source: greeter.proto package greeter import ( fmt "fmt" proto "github.com/golang/protobuf/proto" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type HelloRequest struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *HelloRequest) Reset() { *m = HelloRequest{} } func (m *HelloRequest) String() string { return proto.CompactTextString(m) } func (*HelloRequest) ProtoMessage() {} func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor_e585294ab3f34af5, []int{0} } func (m *HelloRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_HelloRequest.Unmarshal(m, b) } func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic) } func (m *HelloRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_HelloRequest.Merge(m, src) } func (m *HelloRequest) XXX_Size() int { return xxx_messageInfo_HelloRequest.Size(m) } func (m *HelloRequest) XXX_DiscardUnknown() { xxx_messageInfo_HelloRequest.DiscardUnknown(m) } var xxx_messageInfo_HelloRequest proto.InternalMessageInfo func (m *HelloRequest) GetName() string { if m != nil { return m.Name } return "" } type HelloResponse struct { Greeting string `protobuf:"bytes,2,opt,name=greeting,proto3" json:"greeting,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *HelloResponse) Reset() { *m = HelloResponse{} } func (m *HelloResponse) String() string { return proto.CompactTextString(m) } func (*HelloResponse) ProtoMessage() {} func (*HelloResponse) Descriptor() ([]byte, []int) { return fileDescriptor_e585294ab3f34af5, []int{1} } func (m *HelloResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_HelloResponse.Unmarshal(m, b) } func (m *HelloResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_HelloResponse.Marshal(b, m, deterministic) } func (m *HelloResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_HelloResponse.Merge(m, src) } func (m *HelloResponse) XXX_Size() int { return xxx_messageInfo_HelloResponse.Size(m) } func (m *HelloResponse) XXX_DiscardUnknown() { xxx_messageInfo_HelloResponse.DiscardUnknown(m) } var xxx_messageInfo_HelloResponse proto.InternalMessageInfo func (m *HelloResponse) GetGreeting() string { if m != nil { return m.Greeting } return "" } func init() { proto.RegisterType((*HelloRequest)(nil), "HelloRequest") proto.RegisterType((*HelloResponse)(nil), "HelloResponse") } func init() { proto.RegisterFile("greeter.proto", fileDescriptor_e585294ab3f34af5) } var fileDescriptor_e585294ab3f34af5 = []byte{ // 130 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x2f, 0x4a, 0x4d, 0x2d, 0x49, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x52, 0xe2, 0xe2, 0xf1, 0x48, 0xcd, 0xc9, 0xc9, 0x0f, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0x11, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d, 0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x95, 0xb4, 0xb9, 0x78, 0xa1, 0x6a, 0x8a, 0x0b, 0xf2, 0xf3, 0x8a, 0x53, 0x85, 0xa4, 0xb8, 0x38, 0xc0, 0xa6, 0x64, 0xe6, 0xa5, 0x4b, 0x30, 0x81, 0x15, 0xc2, 0xf9, 0x46, 0xc6, 0x5c, 0xec, 0xee, 0x10, 0x1b, 0x84, 0x34, 0xb8, 0x58, 0xc1, 0xfa, 0x84, 0x78, 0xf5, 0x90, 0xed, 0x90, 0xe2, 0xd3, 0x43, 0x31, 0x4e, 0x89, 0x21, 0x89, 0x0d, 0xec, 0x18, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbd, 0xe0, 0x75, 0x0a, 0x9d, 0x00, 0x00, 0x00, } go-agent-3.42.0/v3/integrations/nrmicro/example/proto/greeter.proto000066400000000000000000000004371510742411500253340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; service Greeter { rpc Hello(HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string greeting = 2; } go-agent-3.42.0/v3/integrations/nrmicro/example/pubsub/000077500000000000000000000000001510742411500227435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/example/pubsub/main.go000066400000000000000000000034641510742411500242250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "log" "os" "time" "github.com/micro/go-micro" "github.com/newrelic/go-agent/v3/integrations/nrmicro" proto "github.com/newrelic/go-agent/v3/integrations/nrmicro/example/proto" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func subEv(ctx context.Context, msg *proto.HelloRequest) error { fmt.Println("Message received from", msg.GetName()) return nil } func publish(s micro.Service, app *newrelic.Application) { c := s.Client() for range time.NewTicker(time.Second).C { txn := app.StartTransaction("publish") msg := c.NewMessage("example.topic.pubsub", &proto.HelloRequest{Name: "Sally"}) ctx := newrelic.NewContext(context.Background(), txn) fmt.Println("Sending message") if err := c.Publish(ctx, msg); nil != err { log.Fatal(err) } txn.End() } } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Micro Pub/Sub"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } err = app.WaitForConnection(10 * time.Second) if nil != err { panic(err) } defer app.Shutdown(10 * time.Second) s := micro.NewService( micro.Name("go.micro.srv.pubsub"), // Add the New Relic wrapper to the client which will create // MessageProducerSegments for each Publish call. micro.WrapClient(nrmicro.ClientWrapper()), // Add the New Relic wrapper to the subscriber which will start a new // transaction for each Subscriber invocation. micro.WrapSubscriber(nrmicro.SubscriberWrapper(app)), ) s.Init() go publish(s, app) micro.RegisterSubscriber("example.topic.pubsub", s.Server(), subEv) if err := s.Run(); err != nil { log.Fatal(err) } } go-agent-3.42.0/v3/integrations/nrmicro/example/server/000077500000000000000000000000001510742411500227515ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmicro/example/server/server.go000066400000000000000000000027041510742411500246110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "log" "os" "time" "github.com/micro/go-micro" "github.com/newrelic/go-agent/v3/integrations/nrmicro" proto "github.com/newrelic/go-agent/v3/integrations/nrmicro/example/proto" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) // Greeter is the server struct type Greeter struct{} // Hello is the method on the server being called func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error { name := req.GetName() txn := newrelic.FromContext(ctx) txn.AddAttribute("Name", name) fmt.Println("Request received from", name) rsp.Greeting = "Hello " + name return nil } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Micro Server"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } err = app.WaitForConnection(10 * time.Second) if nil != err { panic(err) } defer app.Shutdown(10 * time.Second) service := micro.NewService( micro.Name("greeter"), // Add the New Relic middleware which will start a new transaction for // each Handler invocation. micro.WrapHandler(nrmicro.HandlerWrapper(app)), ) service.Init() proto.RegisterGreeterHandler(service.Server(), new(Greeter)) if err := service.Run(); err != nil { log.Fatal(err) } } go-agent-3.42.0/v3/integrations/nrmicro/go.mod000066400000000000000000000006321510742411500211170ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrmicro // As of Dec 2019, the go-micro go.mod file uses 1.13: // https://github.com/micro/go-micro/blob/master/go.mod go 1.24 toolchain go1.24.2 require ( github.com/golang/protobuf v1.5.4 github.com/micro/go-micro v1.8.0 github.com/newrelic/go-agent/v3 v3.42.0 google.golang.org/protobuf v1.36.6 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrmicro/nrmicro.go000066400000000000000000000225061510742411500220150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrmicro import ( "context" "io" "net/http" "net/url" "strings" "github.com/micro/go-micro/client" "github.com/micro/go-micro/errors" "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" "github.com/micro/go-micro/server" protoV1 "github.com/golang/protobuf/proto" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" protoV2 "google.golang.org/protobuf/proto" ) type nrWrapper struct { client.Client } var addrMap = make(map[string]string) func startExternal(ctx context.Context, procedure, host string) (context.Context, newrelic.ExternalSegment) { var seg newrelic.ExternalSegment if txn := newrelic.FromContext(ctx); nil != txn { seg = newrelic.ExternalSegment{ StartTime: txn.StartSegmentNow(), Procedure: procedure, Library: "Micro", Host: host, } ctx = addDTPayloadToContext(ctx, txn) } return ctx, seg } func startMessage(ctx context.Context, topic string) (context.Context, *newrelic.MessageProducerSegment) { var seg *newrelic.MessageProducerSegment if txn := newrelic.FromContext(ctx); nil != txn { seg = &newrelic.MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "Micro", DestinationType: newrelic.MessageTopic, DestinationName: topic, } ctx = addDTPayloadToContext(ctx, txn) } return ctx, seg } func addDTPayloadToContext(ctx context.Context, txn *newrelic.Transaction) context.Context { hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) > 0 { md, _ := metadata.FromContext(ctx) md = metadata.Copy(md) for k := range hdrs { if v := hdrs.Get(k); v != "" { md[k] = v } } ctx = metadata.NewContext(ctx, md) } return ctx } func extractHost(addr string) string { if host, ok := addrMap[addr]; ok { return host } host := addr if strings.HasPrefix(host, "unix://") { host = "localhost" } else if u, err := url.Parse(host); nil == err { if "" != u.Host { host = u.Host } else { host = u.Path } } addrMap[addr] = host return host } func (n *nrWrapper) Publish(ctx context.Context, msg client.Message, opts ...client.PublishOption) error { ctx, seg := startMessage(ctx, msg.Topic()) defer seg.End() return n.Client.Publish(ctx, msg, opts...) } func (n *nrWrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { ctx, seg := startExternal(ctx, req.Endpoint(), req.Service()) defer seg.End() return n.Client.Stream(ctx, req, opts...) } func (n *nrWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { ctx, seg := startExternal(ctx, req.Endpoint(), req.Service()) defer seg.End() return n.Client.Call(ctx, req, rsp, opts...) } // ClientWrapper wraps a Micro `client.Client` // (https://godoc.org/github.com/micro/go-micro/client#Client) instance. External // segments will be created for each call to the client's `Call`, `Publish`, or // `Stream` methods. The `newrelic.Transaction` must be put into the context // using `newrelic.NewContext` // (https://godoc.org/github.com/newrelic/go-agent#NewContext) when calling one // of those methods. func ClientWrapper() client.Wrapper { return func(c client.Client) client.Client { return &nrWrapper{c} } } // CallWrapper wraps the `Call` method of a Micro `client.Client` // (https://godoc.org/github.com/micro/go-micro/client#Client) instance. // External segments will be created for each call to the client's `Call` // method. The `newrelic.Transaction` must be put into the context using // `newrelic.NewContext` // (https://godoc.org/github.com/newrelic/go-agent#NewContext) when calling // `Call`. func CallWrapper() client.CallWrapper { return func(cf client.CallFunc) client.CallFunc { return func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error { ctx, seg := startExternal(ctx, req.Endpoint(), req.Service()) defer seg.End() return cf(ctx, node, req, rsp, opts) } } } // HandlerWrapper wraps a Micro `server.Server` // (https://godoc.org/github.com/micro/go-micro/server#Server) handler. // // This wrapper creates transactions for inbound calls. The transaction is // added to the call context and can be accessed in your method handlers using // `newrelic.FromContext` // (https://godoc.org/github.com/newrelic/go-agent#FromContext). // // When an error is returned and it is of type Micro `errors.Error` // (https://godoc.org/github.com/micro/go-micro/errors#Error), the error that // is recorded is based on the HTTP response code (found in the Code field). // Values above 400 or below 100 that are not in the IgnoreStatusCodes // (https://godoc.org/github.com/newrelic/go-agent#Config) configuration list // are recorded as errors. A 500 response code and corresponding error is // recorded when the error is of any other type. A 200 response code is // recorded if no error is returned. func HandlerWrapper(app *newrelic.Application) server.HandlerWrapper { return func(fn server.HandlerFunc) server.HandlerFunc { if app == nil { return fn } return func(ctx context.Context, req server.Request, rsp interface{}) error { txn := startWebTransaction(ctx, app, req) defer txn.End() if req.Body() != nil && newrelic.IsSecurityAgentPresent() { messageType, version := getMessageType(req.Body()) newrelic.GetSecurityAgentInterface().SendEvent("GRPC", req.Body(), messageType, version) } nrrsp := rsp if req.Stream() && newrelic.IsSecurityAgentPresent() { if stream, ok := rsp.(server.Stream); ok { nrrsp = wrappedServerStream{stream} } } err := fn(newrelic.NewContext(ctx, txn), req, nrrsp) var code int if err != nil { if t, ok := err.(*errors.Error); ok { code = int(t.Code) } else { code = 500 } } else { code = 200 } txn.SetWebResponse(nil).WriteHeader(code) return err } } } // SubscriberWrapper wraps a Micro `server.Subscriber` // (https://godoc.org/github.com/micro/go-micro/server#Subscriber) instance. // // This wrapper creates background transactions for inbound calls. The // transaction is added to the subscriber context and can be accessed in your // subscriber handlers using `newrelic.FromContext` // (https://godoc.org/github.com/newrelic/go-agent#FromContext). // // The attribute `"message.routingKey"` is added to the transaction and will // appear on transaction events, transaction traces, error events, and error // traces. It corresponds to the `server.Message`'s Topic // (https://godoc.org/github.com/micro/go-micro/server#Message). // // If a Subscriber returns an error, it will be recorded and reported. func SubscriberWrapper(app *newrelic.Application) server.SubscriberWrapper { return func(fn server.SubscriberFunc) server.SubscriberFunc { if app == nil { return fn } return func(ctx context.Context, m server.Message) (err error) { namer := internal.MessageMetricKey{ Library: "Micro", DestinationType: string(newrelic.MessageTopic), DestinationName: m.Topic(), Consumer: true, } txn := app.StartTransaction(namer.Name()) defer txn.End() integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageRoutingKey, m.Topic(), nil) if md, ok := metadata.FromContext(ctx); ok { hdrs := http.Header{} for k, v := range md { hdrs.Set(k, v) } txn.AcceptDistributedTraceHeaders(newrelic.TransportHTTP, hdrs) } ctx = newrelic.NewContext(ctx, txn) err = fn(ctx, m) if err != nil { txn.NoticeError(err) } return err } } } func startWebTransaction(ctx context.Context, app *newrelic.Application, req server.Request) *newrelic.Transaction { var hdrs http.Header if md, ok := metadata.FromContext(ctx); ok { hdrs = make(http.Header, len(md)) for k, v := range md { hdrs.Add(k, v) } } txn := app.StartTransaction(req.Endpoint()) u := &url.URL{ Scheme: "micro", Host: req.Service(), Path: req.Endpoint(), } webReq := newrelic.WebRequest{ Header: hdrs, URL: u, Method: req.Method(), Transport: newrelic.TransportHTTP, Type: "micro", } txn.SetWebRequest(webReq) return txn } type wrappedServerStream struct { stream server.Stream } func (s wrappedServerStream) Context() context.Context { return s.stream.Context() } func (s wrappedServerStream) Request() server.Request { return s.stream.Request() } func (s wrappedServerStream) Send(msg any) error { return s.stream.Send(msg) } func (s wrappedServerStream) Recv(msg any) error { err := s.stream.Recv(msg) if err != io.EOF { messageType, version := getMessageType(msg) newrelic.GetSecurityAgentInterface().SendEvent("GRPC", msg, messageType, version) } return err } func (s wrappedServerStream) Error() error { return s.stream.Error() } func (s wrappedServerStream) Close() error { return s.stream.Close() } func getMessageType(req any) (string, string) { messageType := "" version := "v2" messagev2, ok := req.(protoV2.Message) if ok { messageType = string(messagev2.ProtoReflect().Descriptor().FullName()) } else { messagev1, ok := req.(protoV1.Message) if ok { messageType = string(protoV1.MessageReflect(messagev1).Descriptor().FullName()) version = "v1" } } return messageType, version } go-agent-3.42.0/v3/integrations/nrmicro/nrmicro_doc.go000066400000000000000000000170761510742411500226500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrmicro instruments https://github.com/micro/go-micro. // // This package can be used to instrument Micro Servers, Clients, Producers, // and Subscribers. // // # Micro Servers // // To instrument a Micro Server, use the `micro.WrapHandler` // (https://godoc.org/github.com/micro/go-micro#WrapHandler) option with // `nrmicro.HandlerWrapper` and your `newrelic.Application` and pass it to the // `micro.NewService` method. Example: // // cfg := newrelic.NewConfig("Micro Server", os.Getenv("NEW_RELIC_LICENSE_KEY")) // app, _ := newrelic.NewApplication(cfg) // service := micro.NewService( // micro.WrapHandler(nrmicro.HandlerWrapper(app)), // ) // // Alternatively, use the `server.WrapHandler` // (https://godoc.org/github.com/micro/go-micro/server#WrapHandler) option with // `nrmicro.HandlerWrapper` and your `newrelic.Application` and pass it to the // `server.NewServer` method. Example: // // cfg := newrelic.NewConfig("Micro Server", os.Getenv("NEW_RELIC_LICENSE_KEY")) // app, _ := newrelic.NewApplication(cfg) // svr := server.NewServer( // server.WrapHandler(nrmicro.HandlerWrapper(app)), // ) // // If more than one wrapper is passed to `micro.WrapHandler` or // `server.WrapHandler` as a list, be sure that the `nrmicro.HandlerWrapper` is // first in this list. // // This wrapper creates transactions for inbound calls. The transaction is // added to the call context and can be accessed in your method handlers using // `newrelic.FromContext` // (https://godoc.org/github.com/newrelic/go-agent#FromContext). // // When an error is returned and it is of type Micro `errors.Error` // (https://godoc.org/github.com/micro/go-micro/errors#Error), the error that // is recorded is based on the HTTP response code (found in the Code field). // Values above 400 or below 100 that are not in the IgnoreStatusCodes // (https://godoc.org/github.com/newrelic/go-agent#Config) configuration list // are recorded as errors. A 500 response code and corresponding error is // recorded when the error is of any other type. A 200 response code is // recorded if no error is returned. // // Full server example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmicro/example/server/server.go // // # Micro Clients // // There are three different ways to instrument a Micro Client and create // External segments for `Call`, `Publish`, and `Stream` methods. // // No matter which way the Micro `client.Client` is wrapped, all calls to // `Client.Call`, `Client.Publish`, or `Client.Stream` must be done with a // context which contains a `newrelic.Transaction`. // // ctx = newrelic.NewContext(ctx, txn) // err := cli.Call(ctx, req, &rsp) // // 1. The first option is to wrap the `Call`, `Publish`, and `Stream` methods // on a client using the `micro.WrapClient` // (https://godoc.org/github.com/micro/go-micro#WrapClient) option with // `nrmicro.ClientWrapper` and pass it to the `micro.NewService` method. If // more than one wrapper is passed to `micro.WrapClient`, ensure that the // `nrmicro.ClientWrapper` is the first in the list. `ExternalSegment`s will be // created each time a `Call` or `Stream` method is called on the // client. `MessageProducerSegment`s will be created each time a `Publish` // method is called on the client. Example: // // service := micro.NewService( // micro.WrapClient(nrmicro.ClientWrapper()), // ) // cli := service.Client() // // It is also possible to use the `client.Wrap` // (https://godoc.org/github.com/micro/go-micro/client#Wrap) option with // `nrmicro.ClientWrapper` and pass it to the `client.NewClient` method to // achieve the same result. // // cli := client.NewClient( // client.Wrap(nrmicro.ClientWrapper()), // ) // // 2. The second option is to wrap just the `Call` method on a client using the // `micro.WrapCall` (https://godoc.org/github.com/micro/go-micro#WrapCall) // option with `nrmicro.CallWrapper` and pass it to the `micro.NewService` // method. If more than one wrapper is passed to `micro.WrapCall`, ensure that // the `nrmicro.CallWrapper` is the first in the list. External segments will // be created each time a `Call` method is called on the client. Example: // // service := micro.NewService( // micro.WrapCall(nrmicro.CallWrapper()), // ) // cli := service.Client() // // It is also possible to use the `client.WrapCall` // (https://godoc.org/github.com/micro/go-micro/client#WrapCall) option with // `nrmicro.CallWrapper` and pass it to the `client.NewClient` method to // achieve the same result. // // cli := client.NewClient( // client.WrapCall(nrmicro.CallWrapper()), // ) // // 3. The third option is to wrap the Micro Client directly using // `nrmicro.ClientWrapper`. `ExternalSegment`s will be created each time a // `Call` or `Stream` method is called on the client. // `MessageProducerSegment`s will be created each time a `Publish` method is // called on the client. Example: // // cli := client.NewClient() // cli = nrmicro.ClientWrapper()(cli) // // Full client example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmicro/example/client/client.go // // # Micro Producers // // To instrument a Micro Producer, wrap the Micro Client using the // `nrmico.ClientWrapper` as described in option 1 or 3 above. // `MessageProducerSegment`s will be created each time a `Publish` method is // called on the client. Be sure the context passed to the `Publish` method // contains a `newrelic.Transaction`. // // service := micro.NewService( // micro.WrapClient(nrmicro.ClientWrapper()), // ) // cli := service.Client() // // // Add the transaction to the context // ctx := newrelic.NewContext(context.Background(), txn) // msg := cli.NewMessage("my.example.topic", "hello world") // err := cli.Publish(ctx, msg) // // Full Publisher/Subscriber example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmicro/example/pubsub/main.go // // # Micro Subscribers // // To instrument a Micro Subscriber use the `micro.WrapSubscriber` // (https://godoc.org/github.com/micro/go-micro#WrapSubscriber) option with // `nrmicro.SubscriberWrapper` and your `newrelic.Application` and pass it to // the `micro.NewService` method. Example: // // cfg := newrelic.NewConfig("Micro Subscriber", os.Getenv("NEW_RELIC_LICENSE_KEY")) // app, _ := newrelic.NewApplication(cfg) // service := micro.NewService( // micro.WrapSubscriber(nrmicro.SubscriberWrapper(app)), // ) // // Alternatively, use the `server.WrapSubscriber` // (https://godoc.org/github.com/micro/go-micro/server#WrapSubscriber) option // with `nrmicro.SubscriberWrapper` and your `newrelic.Application` and pass it // to the `server.NewServer` method. Example: // // cfg := newrelic.NewConfig("Micro Subscriber", os.Getenv("NEW_RELIC_LICENSE_KEY")) // app, _ := newrelic.NewApplication(cfg) // svr := server.NewServer( // server.WrapSubscriber(nrmicro.SubscriberWrapper(app)), // ) // // If more than one wrapper is passed to `micro.WrapSubscriber` or // `server.WrapSubscriber` as a list, be sure that the `nrmicro.SubscriberWrapper` is // first in this list. // // This wrapper creates background transactions for inbound calls. The // transaction is added to the subscriber context and can be accessed in your // subscriber handlers using `newrelic.FromContext`. // // If a Subscriber returns an error, it will be recorded and reported. // // Full Publisher/Subscriber example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmicro/example/pubsub/main.go package nrmicro import "github.com/newrelic/go-agent/v3/internal" func init() { internal.TrackUsage("integration", "framework", "micro") } go-agent-3.42.0/v3/integrations/nrmicro/nrmicro_test.go000066400000000000000000001160231510742411500230520ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrmicro import ( "context" "errors" "sync" "testing" "time" "github.com/micro/go-micro" "github.com/micro/go-micro/broker" bmemory "github.com/micro/go-micro/broker/memory" "github.com/micro/go-micro/client" "github.com/micro/go-micro/client/selector" microerrors "github.com/micro/go-micro/errors" "github.com/micro/go-micro/metadata" rmemory "github.com/micro/go-micro/registry/memory" "github.com/micro/go-micro/server" proto "github.com/newrelic/go-agent/v3/integrations/nrmicro/example/proto" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) const ( missingHeaders = "HEADERS NOT FOUND" missingMetadata = "METADATA NOT FOUND" serverName = "testing" topic = "topic" ) type TestRequest struct{} type TestResponse struct { RequestHeaders string } func dtHeadersFound(hdr string) bool { return hdr != "" && hdr != missingMetadata && hdr != missingHeaders } type TestHandler struct{} func (t *TestHandler) Method(ctx context.Context, req *TestRequest, rsp *TestResponse) error { rsp.RequestHeaders = getDTRequestHeaderVal(ctx) defer newrelic.FromContext(ctx).StartSegment("Method").End() return nil } func (t *TestHandler) StreamingMethod(ctx context.Context, stream server.Stream) error { if err := stream.Recv(new(string)); nil != err { return err } if err := stream.Send(getDTRequestHeaderVal(ctx)); nil != err { return err } return nil } type TestHandlerWithError struct{} func (t *TestHandlerWithError) Method(ctx context.Context, req *TestRequest, rsp *TestResponse) error { rsp.RequestHeaders = getDTRequestHeaderVal(ctx) return microerrors.Unauthorized("id", "format") } type TestHandlerWithNonMicroError struct{} func (t *TestHandlerWithNonMicroError) Method(ctx context.Context, req *TestRequest, rsp *TestResponse) error { rsp.RequestHeaders = getDTRequestHeaderVal(ctx) return errors.New("Non-Micro Error") } func getDTRequestHeaderVal(ctx context.Context) string { if md, ok := metadata.FromContext(ctx); ok { if dtHeader, ok := md[newrelic.DistributedTraceNewRelicHeader]; ok { return dtHeader } return missingHeaders } return missingMetadata } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, cfgFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.AccountID = "123" reply.TrustedAccountKey = "123" reply.PrimaryAppID = "456" } var cfgFn = func(cfg *newrelic.Config) { cfg.Attributes.Include = append(cfg.Attributes.Include, newrelic.AttributeMessageRoutingKey, newrelic.AttributeMessageQueueName, newrelic.AttributeMessageExchangeType, newrelic.AttributeMessageReplyTo, newrelic.AttributeMessageCorrelationID, ) } func newTestWrappedClientAndServer(app *newrelic.Application, wrapperOption client.Option, t *testing.T) (client.Client, server.Server) { registry := rmemory.NewRegistry() sel := selector.NewSelector(selector.Registry(registry)) c := client.NewClient( client.Selector(sel), wrapperOption, ) s := server.NewServer( server.Name(serverName), server.Registry(registry), server.WrapHandler(HandlerWrapper(app)), ) s.Handle(s.NewHandler(new(TestHandler))) s.Handle(s.NewHandler(new(TestHandlerWithError))) s.Handle(s.NewHandler(new(TestHandlerWithNonMicroError))) if err := s.Start(); nil != err { t.Fatal(err) } return c, s } func TestClientCallWithNoTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.Wrap(ClientWrapper()), t) defer s.Stop() testClientCallWithNoTransaction(c, t) } func TestClientCallWrapperWithNoTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.WrapCall(CallWrapper()), t) defer s.Stop() testClientCallWithNoTransaction(c, t) } func testClientCallWithNoTransaction(c client.Client, t *testing.T) { ctx := context.Background() req := c.NewRequest(serverName, "TestHandler.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} if err := c.Call(ctx, req, &rsp); nil != err { t.Fatal("Error calling test client:", err) } if rsp.RequestHeaders != missingHeaders { t.Error("Header should not be here", rsp.RequestHeaders) } } func TestClientCallWithTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.Wrap(ClientWrapper()), t) defer s.Stop() testClientCallWithTransaction(c, t) } func TestClientCallWrapperWithTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.WrapCall(CallWrapper()), t) defer s.Stop() testClientCallWithTransaction(c, t) } func testClientCallWithTransaction(c client.Client, t *testing.T) { req := c.NewRequest(serverName, "TestHandler.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} app := createTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) if err := c.Call(ctx, req, &rsp); nil != err { t.Fatal("Error calling test client:", err) } if !dtHeadersFound(rsp.RequestHeaders) { t.Error("Incorrect header:", rsp.RequestHeaders) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/name", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/name", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/testing/all", Scope: "", Forced: false, Data: nil}, {Name: "External/testing/Micro/TestHandler.Method", Scope: "OtherTransaction/Go/name", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "Micro", "name": "External/testing/Micro/TestHandler.Method", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/name", "transaction.name": "OtherTransaction/Go/name", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/name", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/name", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/testing/Micro/TestHandler.Method", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestClientCallMetadata(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.Wrap(ClientWrapper()), t) defer s.Stop() testClientCallMetadata(c, t) } func TestCallMetadata(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.WrapCall(CallWrapper()), t) defer s.Stop() testClientCallMetadata(c, t) } func testClientCallMetadata(c client.Client, t *testing.T) { // test that context metadata is not changed by the newrelic wrapper req := c.NewRequest(serverName, "TestHandler.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} app := createTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) md := metadata.Metadata{ "zip": "zap", } ctx = metadata.NewContext(ctx, md) if err := c.Call(ctx, req, &rsp); nil != err { t.Fatal("Error calling test client:", err) } if len(md) != 1 || md["zip"] != "zap" { t.Error("metadata changed:", md) } } func waitOrTimeout(t *testing.T, wg *sync.WaitGroup) { ch := make(chan struct{}) go func() { defer close(ch) wg.Wait() }() select { case <-ch: case <-time.After(time.Second): t.Fatal("timeout waiting for message") } } func TestClientPublishWithNoTransaction(t *testing.T) { c, _, b := newTestClientServerAndBroker(createTestApp().Application, t) var wg sync.WaitGroup if err := b.Connect(); nil != err { t.Fatal("broker connect error:", err) } defer b.Disconnect() if _, err := b.Subscribe(topic, func(e broker.Event) error { defer wg.Done() h := e.Message().Header if _, ok := h[newrelic.DistributedTraceNewRelicHeader]; ok { t.Error("Distributed tracing headers found", h) } return nil }); nil != err { t.Fatal("Failure to subscribe to broker:", err) } ctx := context.Background() msg := c.NewMessage(topic, "hello world") wg.Add(1) if err := c.Publish(ctx, msg); nil != err { t.Fatal("Error calling test client:", err) } waitOrTimeout(t, &wg) } func TestClientPublishWithTransaction(t *testing.T) { c, _, b := newTestClientServerAndBroker(createTestApp().Application, t) var wg sync.WaitGroup if err := b.Connect(); nil != err { t.Fatal("broker connect error:", err) } defer b.Disconnect() if _, err := b.Subscribe(topic, func(e broker.Event) error { defer wg.Done() h := e.Message().Header if _, ok := h[newrelic.DistributedTraceNewRelicHeader]; !ok { t.Error("Distributed tracing headers not found", h) } return nil }); nil != err { t.Fatal("Failure to subscribe to broker:", err) } app := createTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) msg := c.NewMessage(topic, "hello world") wg.Add(1) if err := c.Publish(ctx, msg); nil != err { t.Fatal("Error calling test client:", err) } waitOrTimeout(t, &wg) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/Micro/Topic/Produce/Named/topic", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/Micro/Topic/Produce/Named/topic", Scope: "OtherTransaction/Go/name", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/name", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/name", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "MessageBroker/Micro/Topic/Produce/Named/topic", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/name", "transaction.name": "OtherTransaction/Go/name", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/name", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/name", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "MessageBroker/Micro/Topic/Produce/Named/topic", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestExtractHost(t *testing.T) { testcases := []struct { input string expect string }{ { input: "192.168.0.10", expect: "192.168.0.10", }, { input: "192.168.0.10:1234", expect: "192.168.0.10:1234", }, { input: "unix:///path/to/file", expect: "localhost", }, { input: "nats://127.0.0.1:4222", expect: "127.0.0.1:4222", }, { input: "scheme://user:pass@host.com:5432/path?k=v#f", expect: "host.com:5432", }, } for _, test := range testcases { if actual := extractHost(test.input); actual != test.expect { t.Errorf("incorrect host value extracted: actual=%s expected=%s", actual, test.expect) } } } func TestClientStreamWrapperWithNoTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.Wrap(ClientWrapper()), t) defer s.Stop() ctx := context.Background() req := c.NewRequest( serverName, "TestHandler.StreamingMethod", &TestRequest{}, client.WithContentType("application/json"), client.StreamingRequest(), ) stream, err := c.Stream(ctx, req) defer stream.Close() if nil != err { t.Fatal("Error calling test client:", err) } var resp string if err := stream.Send(&resp); nil != err { t.Fatal(err) } err = stream.Recv(&resp) if nil != err { t.Fatal(err) } if dtHeadersFound(resp) { t.Error("dt headers found:", resp) } err = stream.Recv(&resp) if nil == err { t.Fatal("should have received EOF error from server") } } func TestClientStreamWrapperWithTransaction(t *testing.T) { c, s := newTestWrappedClientAndServer(createTestApp().Application, client.Wrap(ClientWrapper()), t) defer s.Stop() app := createTestApp() txn := app.StartTransaction("name") ctx := newrelic.NewContext(context.Background(), txn) req := c.NewRequest( serverName, "TestHandler.StreamingMethod", &TestRequest{}, client.WithContentType("application/json"), client.StreamingRequest(), ) stream, err := c.Stream(ctx, req) defer stream.Close() if nil != err { t.Fatal("Error calling test client:", err) } var resp string // second outgoing request to server, ensures we only create a single // metric for the entire streaming cycle if err := stream.Send(&resp); nil != err { t.Fatal(err) } // receive the distributed trace headers from the server if err := stream.Recv(&resp); nil != err { t.Fatal(err) } if !dtHeadersFound(resp) { t.Error("dt headers not found:", resp) } // exhaust the stream if err := stream.Recv(&resp); nil == err { t.Fatal("should have received EOF error from server") } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/name", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/name", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/testing/all", Scope: "", Forced: false, Data: nil}, {Name: "External/testing/Micro/TestHandler.StreamingMethod", Scope: "OtherTransaction/Go/name", Forced: false, Data: []float64{1}}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "Micro", "name": "External/testing/Micro/TestHandler.StreamingMethod", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/name", "transaction.name": "OtherTransaction/Go/name", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/name", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/name", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "External/testing/Micro/TestHandler.StreamingMethod", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestServerWrapperWithNoApp(t *testing.T) { c, s := newTestWrappedClientAndServer(nil, client.Wrap(ClientWrapper()), t) defer s.Stop() ctx := context.Background() req := c.NewRequest(serverName, "TestHandler.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} if err := c.Call(ctx, req, &rsp); nil != err { t.Fatal("Error calling test client:", err) } if rsp.RequestHeaders != missingHeaders { t.Error("Header should not be here", rsp.RequestHeaders) } } func TestServerWrapperWithApp(t *testing.T) { app := createTestApp() c, s := newTestWrappedClientAndServer(app.Application, client.Wrap(ClientWrapper()), t) defer s.Stop() ctx := context.Background() txn := app.StartTransaction("txn") defer txn.End() ctx = newrelic.NewContext(ctx, txn) req := c.NewRequest(serverName, "TestHandler.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} if err := c.Call(ctx, req, &rsp); nil != err { t.Fatal("Error calling test client:", err) } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/TestHandler.Method", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction/Go/TestHandler.Method", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestHandler.Method", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Method", Scope: "", Forced: false, Data: nil}, {Name: "Custom/Method", Scope: "WebTransaction/Go/TestHandler.Method", Forced: false, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/Method", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestHandler.Method", "transaction.name": "WebTransaction/Go/TestHandler.Method", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.method": "TestHandler.Method", "request.uri": "micro://testing/TestHandler.Method", "request.headers.accept": "application/json", "request.headers.contentType": "application/json", "request.headers.contentLength": 3, "httpResponseCode": "200", "http.statusCode": 200, }, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/TestHandler.Method", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/TestHandler.Method", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/Method", Attributes: map[string]interface{}{}, }, }, }}, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/TestHandler.Method", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": "S", "parent.account": 123, "parent.transportType": "HTTP", "parent.app": 456, "parentId": internal.MatchAnything, "parent.type": "App", "parent.transportDuration": internal.MatchAnything, "parentSpanId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "TestHandler.Method", "request.uri": "micro://testing/TestHandler.Method", "request.headers.accept": "application/json", "request.headers.contentType": "application/json", "request.headers.contentLength": 3, "httpResponseCode": 200, "http.statusCode": 200, }, }}) } func TestServerWrapperWithAppReturnsError(t *testing.T) { app := createTestApp() c, s := newTestWrappedClientAndServer(app.Application, client.Wrap(ClientWrapper()), t) defer s.Stop() ctx := context.Background() req := c.NewRequest(serverName, "TestHandlerWithError.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} if err := c.Call(ctx, req, &rsp); nil == err { t.Fatal("Expected an error but did not get one") } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex/Go/TestHandlerWithError.Method", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Errors/WebTransaction/Go/TestHandlerWithError.Method", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction/Go/TestHandlerWithError.Method", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestHandlerWithError.Method", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "WebTransaction/Go/TestHandlerWithError.Method", "transaction.name": "WebTransaction/Go/TestHandlerWithError.Method", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ newrelic.SpanAttributeErrorClass: "401", newrelic.SpanAttributeErrorMessage: "Unauthorized", "request.method": "TestHandlerWithError.Method", "request.uri": "micro://testing/TestHandlerWithError.Method", "request.headers.accept": "application/json", "request.headers.contentType": "application/json", "request.headers.contentLength": 3, "httpResponseCode": "401", "http.statusCode": 401, }, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/TestHandlerWithError.Method", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/TestHandlerWithError.Method", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{}, }}, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/TestHandlerWithError.Method", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "TestHandlerWithError.Method", "request.uri": "micro://testing/TestHandlerWithError.Method", "request.headers.accept": "application/json", "request.headers.contentType": "application/json", "request.headers.contentLength": 3, "httpResponseCode": 401, "http.statusCode": 401, }, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/TestHandlerWithError.Method", Msg: "Unauthorized", Klass: "401", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": "Unauthorized", "error.class": "401", "transactionName": "WebTransaction/Go/TestHandlerWithError.Method", "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "guid": internal.MatchAnything, "spanId": internal.MatchAnything, "sampled": "true", }, }}) } func TestServerWrapperWithAppReturnsNonMicroError(t *testing.T) { app := createTestApp() c, s := newTestWrappedClientAndServer(app.Application, client.Wrap(ClientWrapper()), t) defer s.Stop() ctx := context.Background() req := c.NewRequest("testing", "TestHandlerWithNonMicroError.Method", &TestRequest{}, client.WithContentType("application/json")) rsp := TestResponse{} if err := c.Call(ctx, req, &rsp); nil == err { t.Fatal("Expected an error but did not get one") } app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Apdex/Go/TestHandlerWithNonMicroError.Method", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Errors/WebTransaction/Go/TestHandlerWithNonMicroError.Method", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "WebTransaction/Go/TestHandlerWithNonMicroError.Method", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/TestHandlerWithNonMicroError.Method", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/TestHandlerWithNonMicroError.Method", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "TestHandlerWithNonMicroError.Method", "request.uri": "micro://testing/TestHandlerWithNonMicroError.Method", "request.headers.accept": "application/json", "request.headers.contentType": "application/json", "request.headers.contentLength": 3, "httpResponseCode": 500, "http.statusCode": 500, }, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/TestHandlerWithNonMicroError.Method", Msg: "Internal Server Error", Klass: "500", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": "Internal Server Error", "error.class": "500", "transactionName": "WebTransaction/Go/TestHandlerWithNonMicroError.Method", "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "guid": internal.MatchAnything, "spanId": internal.MatchAnything, "sampled": "true", }, }}) } func TestServerSubscribeNoApp(t *testing.T) { c, s, b := newTestClientServerAndBroker(nil, t) defer s.Stop() var wg sync.WaitGroup if err := b.Connect(); nil != err { t.Fatal("broker connect error:", err) } defer b.Disconnect() err := micro.RegisterSubscriber(topic, s, func(ctx context.Context, msg *proto.HelloRequest) error { defer wg.Done() return nil }) if err != nil { t.Fatal("error registering subscriber", err) } if err := s.Start(); nil != err { t.Fatal(err) } ctx := context.Background() msg := c.NewMessage(topic, &proto.HelloRequest{Name: "test"}) wg.Add(1) if err := c.Publish(ctx, msg); nil != err { t.Fatal("Error calling publish:", err) } waitOrTimeout(t, &wg) } func TestServerSubscribe(t *testing.T) { app := createTestApp() c, s, _ := newTestClientServerAndBroker(app.Application, t) var wg sync.WaitGroup err := micro.RegisterSubscriber(topic, s, func(ctx context.Context, msg *proto.HelloRequest) error { txn := newrelic.FromContext(ctx) defer txn.StartSegment("segment").End() defer wg.Done() return nil }) if err != nil { t.Fatal("error registering subscriber", err) } if err := s.Start(); nil != err { t.Fatal(err) } ctx := context.Background() msg := c.NewMessage(topic, &proto.HelloRequest{Name: "test"}) wg.Add(1) txn := app.StartTransaction("pub") ctx = newrelic.NewContext(ctx, txn) if err := c.Publish(ctx, msg); nil != err { t.Fatal("Error calling publish:", err) } defer txn.End() waitOrTimeout(t, &wg) s.Stop() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/Message/Micro/Topic/Named/topic", Scope: "", Forced: false, Data: nil}, {Name: "Custom/segment", Scope: "", Forced: false, Data: nil}, {Name: "Custom/segment", Scope: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "Custom/segment", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "transaction.name": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "nr.entryPoint": true, "parentId": internal.MatchAnything, "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "message.routingKey": "topic", "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", }, }, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "name": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "parent.account": 123, "parent.app": 456, "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "parentId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "message.routingKey": "topic", }, UserAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{{ SegmentName: "Custom/segment", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}}, }, }}, }, }}) } func TestServerSubscribeWithError(t *testing.T) { app := createTestApp() c, s, _ := newTestClientServerAndBroker(app.Application, t) var wg sync.WaitGroup err := micro.RegisterSubscriber(topic, s, func(ctx context.Context, msg *proto.HelloRequest) error { defer wg.Done() return errors.New("subscriber error") }) if err != nil { t.Fatal("error registering subscriber", err) } if err := s.Start(); nil != err { t.Fatal(err) } ctx := context.Background() msg := c.NewMessage(topic, &proto.HelloRequest{Name: "test"}) wg.Add(1) if err := c.Publish(ctx, msg); nil == err { t.Fatal("Expected error but didn't get one") } waitOrTimeout(t, &wg) s.Stop() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/Message/Micro/Topic/Named/topic", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Errors/OtherTransaction/Go/Message/Micro/Topic/Named/topic", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "transaction.name": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ newrelic.SpanAttributeErrorClass: "*errors.errorString", newrelic.SpanAttributeErrorMessage: "subscriber error", "message.routingKey": "topic", }, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{}, }}, }, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/Message/Micro/Topic/Named/topic", Msg: "subscriber error", Klass: "*errors.errorString", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.message": "subscriber error", "error.class": "*errors.errorString", "transactionName": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "guid": internal.MatchAnything, "spanId": internal.MatchAnything, "sampled": "true", }, }}) } func newTestClientServerAndBroker(app *newrelic.Application, t *testing.T) (client.Client, server.Server, broker.Broker) { b := bmemory.NewBroker() c := client.NewClient( client.Broker(b), client.Wrap(ClientWrapper()), ) s := server.NewServer( server.Name(serverName), server.Broker(b), server.WrapSubscriber(SubscriberWrapper(app)), ) return c, s, b } go-agent-3.42.0/v3/integrations/nrmongo-v2/000077500000000000000000000000001510742411500203435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmongo-v2/LICENSE.txt000066400000000000000000000264501510742411500221750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrmongo-v2/README.md000066400000000000000000000007171510742411500216270ustar00rootroot00000000000000# v3/integrations/nrmongo-v2 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo-v2?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo) Package `nrmongo` instruments https://github.com/mongodb/mongo-go-driver v2 ```go import "github.com/newrelic/go-agent/v3/integrations/nrmongo-v2" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo-v2). go-agent-3.42.0/v3/integrations/nrmongo-v2/example/000077500000000000000000000000001510742411500217765ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmongo-v2/example/main.go000066400000000000000000000031231510742411500232500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrmongo-v2" newrelic "github.com/newrelic/go-agent/v3/newrelic" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic Mongo App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } app.WaitForConnection(10 * time.Second) // If you have another CommandMonitor, you can pass it to NewCommandMonitor and it will get called along // with the NR monitor nrCmdMonitor := nrmongo.NewCommandMonitor(nil) ctx := context.Background() // nrCmdMonitor must be added after any other monitors are added, as previous options get overwritten. // This example assumes Mongo is running locally on port 27017 client, err := mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017").SetMonitor(nrCmdMonitor)) if err != nil { panic(err) } defer client.Disconnect(ctx) txn := app.StartTransaction("Mongo txn") // Make sure to add the newrelic.Transaction to the context nrCtx := newrelic.NewContext(context.Background(), txn) collection := client.Database("testing").Collection("numbers") _, err = collection.InsertOne(nrCtx, bson.M{"name": "exampleName", "value": "exampleValue"}) if err != nil { panic(err) } txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrmongo-v2/go.mod000066400000000000000000000005141510742411500214510ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrmongo-v2 // https://github.com/mongodb/mongo-go-driver#requirements go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 // mongo-driver does not support modules as of Nov 2019. go.mongodb.org/mongo-driver/v2 v2.2.2 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrmongo-v2/nrmongo.go000066400000000000000000000133151510742411500223540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrmongo instruments https://github.com/mongodb/mongo-go-driver v2 // // Use this package to instrument your MongoDB calls without having to manually // create DatastoreSegments. To do so, first set the monitor in the connect // options using `SetMonitor` // (https://pkg.go.dev/go.mongodb.org/mongo-driver/v2/mongo/options#ClientOptions.SetMonitor): // // nrCmdMonitor := nrmongo.NewCommandMonitor(nil) // client, err := mongo.Connect(options.Client().SetMonitor(nrCmdMonitor)) // // Note that it is important that this `nrmongo` monitor is the last monitor // set, otherwise it will be overwritten. If needing to use more than one // `event.CommandMonitor`, pass the original monitor to the // `nrmongo.NewCommandMonitor` function: // // origMon := &event.CommandMonitor{ // Started: origStarted, // Succeeded: origSucceeded, // Failed: origFailed, // } // nrCmdMonitor := nrmongo.NewCommandMonitor(origMon) // client, err := mongo.Connect(options.Client().SetMonitor(nrCmdMonitor)) // // Then add the current transaction to the context used in any MongoDB call: // // ctx = newrelic.NewContext(context.Background(), txn) // collection := client.Database("testing").Collection("numbers") // resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) package nrmongo import ( "context" "regexp" "strings" "sync" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" ) func init() { internal.TrackUsage("integration", "datastore", "mongo-v2") } type mongoMonitor struct { segmentMap map[int64]*newrelic.DatastoreSegment origCommMon *event.CommandMonitor sync.Mutex } // The Mongo connection ID is constructed as: `fmt.Sprintf("%s[-%d]", addr, nextConnectionID())`, // where addr is of the form `host:port` (or `a.sock` for unix sockets) // See https://github.com/mongodb/mongo-go-driver/blob/release/2.2/x/mongo/driver/topology/connection.go#L93 // and https://github.com/mongodb/mongo-go-driver/blob/release/2.2/mongo/address/addr.go var connIDPattern = regexp.MustCompile(`([^:\[]+)(?::(\d+))?\[-\d+]`) // NewCommandMonitor returns a new `*event.CommandMonitor` // (https://pkg.go.dev/go.mongodb.org/mongo-driver/v2/event#CommandMonitor). If // provided, the original `*event.CommandMonitor` will be called as well. The // returned `*event.CommandMonitor` creates `newrelic.DatastoreSegment`s // (https://pkg.go.dev/github.com/newrelic/go-agent/v3/newrelic#DatastoreSegment) for each // database call. func NewCommandMonitor(original *event.CommandMonitor) *event.CommandMonitor { m := mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), origCommMon: original, } return &event.CommandMonitor{ Started: m.started, Succeeded: m.succeeded, Failed: m.failed, } } func (m *mongoMonitor) started(ctx context.Context, e *event.CommandStartedEvent) { var secureAgentEvent any if m.origCommMon != nil && m.origCommMon.Started != nil { m.origCommMon.Started(ctx, e) } txn := newrelic.FromContext(ctx) if txn == nil { return } if newrelic.IsSecurityAgentPresent() { commandName := e.CommandName if strings.ToLower(commandName) == "findandmodify" { value, ok := e.Command.Lookup("remove").BooleanOK() if ok && value { commandName = "delete" } } secureAgentEvent = newrelic.GetSecurityAgentInterface().SendEvent("MONGO", getJsonQuery(e.Command), commandName) } host, port := calcHostAndPort(e.ConnectionID) sgmt := newrelic.DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: newrelic.DatastoreMongoDB, Collection: collName(e), Operation: e.CommandName, Host: host, PortPathOrID: port, DatabaseName: e.DatabaseName, } if newrelic.IsSecurityAgentPresent() { sgmt.SetSecureAgentEvent(secureAgentEvent) } m.addSgmt(e, &sgmt) } func collName(e *event.CommandStartedEvent) string { coll := e.Command.Lookup(e.CommandName) collName, _ := coll.StringValueOK() return collName } func (m *mongoMonitor) addSgmt(e *event.CommandStartedEvent, sgmt *newrelic.DatastoreSegment) { m.Lock() defer m.Unlock() m.segmentMap[e.RequestID] = sgmt } func (m *mongoMonitor) succeeded(ctx context.Context, e *event.CommandSucceededEvent) { if sgmt := m.getSgmt(e.RequestID); sgmt != nil && newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendExitEvent(sgmt.GetSecureAgentEvent(), nil) } m.endSgmtIfExists(e.RequestID) if m.origCommMon != nil && m.origCommMon.Succeeded != nil { m.origCommMon.Succeeded(ctx, e) } } func (m *mongoMonitor) failed(ctx context.Context, e *event.CommandFailedEvent) { m.endSgmtIfExists(e.RequestID) if m.origCommMon != nil && m.origCommMon.Failed != nil { m.origCommMon.Failed(ctx, e) } } func (m *mongoMonitor) endSgmtIfExists(id int64) { m.getAndRemoveSgmt(id).End() } func (m *mongoMonitor) getAndRemoveSgmt(id int64) *newrelic.DatastoreSegment { m.Lock() defer m.Unlock() sgmt := m.segmentMap[id] if sgmt != nil { delete(m.segmentMap, id) } return sgmt } func (m *mongoMonitor) getSgmt(id int64) *newrelic.DatastoreSegment { m.Lock() defer m.Unlock() sgmt := m.segmentMap[id] return sgmt } func calcHostAndPort(connID string) (host string, port string) { // FindStringSubmatch either returns nil or an array of the size # of submatches + 1 (in this case 3) addressParts := connIDPattern.FindStringSubmatch(connID) if len(addressParts) == 3 { host = addressParts[1] port = addressParts[2] } return } func getJsonQuery(q interface{}) []byte { map_json, err := bson.MarshalExtJSON(q, true, true) if err != nil { return []byte("") } else { return map_json } } go-agent-3.42.0/v3/integrations/nrmongo-v2/nrmongo_test.go000066400000000000000000000334051510742411500234150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrmongo import ( "context" "errors" "os" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/sysinfo" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) var ( MONGO_HOST = os.Getenv("MONGO_HOST") MONGO_PORT = os.Getenv("MONGO_PORT") MONGO_USER = os.Getenv("MONGO_INITDB_ROOT_USERNAME") MONGO_PASS = os.Getenv("MONGO_INITDB_ROOT_PASSWORD") MONGO_DB = os.Getenv("MONGO_DB") MONGO_URI = "mongodb://" + MONGO_USER + ":" + MONGO_PASS + "@" + MONGO_HOST + ":" + MONGO_PORT connID = "localhost:27017[-1]" reqID int64 = 10 raw, _ = bson.Marshal(bson.D{bson.E{Key: "commName", Value: "collName"}, {Key: "$db", Value: "testing"}}) ste = &event.CommandStartedEvent{ Command: raw, DatabaseName: "testdb", CommandName: "commName", RequestID: reqID, ConnectionID: connID, } finishedEvent = event.CommandFinishedEvent{ Duration: 5, CommandName: "name", RequestID: reqID, ConnectionID: connID, } se = &event.CommandSucceededEvent{ CommandFinishedEvent: finishedEvent, Reply: nil, } fe = &event.CommandFailedEvent{ CommandFinishedEvent: finishedEvent, Failure: errors.New("failureCause"), } thisHost, _ = sysinfo.Hostname() ) func TestOrigMonitorsAreCalled(t *testing.T) { var started, succeeded, failed bool origMonitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { started = true }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { succeeded = true }, Failed: func(ctx context.Context, e *event.CommandFailedEvent) { failed = true }, } ctx := context.Background() nrMonitor := NewCommandMonitor(origMonitor) nrMonitor.Started(ctx, ste) if !started { t.Error("started not called") } nrMonitor.Succeeded(ctx, se) if !succeeded { t.Error("succeeded not called") } nrMonitor.Failed(ctx, fe) if !failed { t.Error("failed not called") } } func TestClientOptsWithNullFunctions(t *testing.T) { origMonitor := &event.CommandMonitor{} // the monitor isn't nil, but its functions are. ctx := context.Background() nrMonitor := NewCommandMonitor(origMonitor) // Verifying no nil pointer dereferences nrMonitor.Started(ctx, ste) nrMonitor.Succeeded(ctx, se) nrMonitor.Failed(ctx, fe) } func TestHostAndPort(t *testing.T) { type hostAndPort struct { host string port string } testCases := map[string]hostAndPort{ "localhost:8080[-1]": {host: "localhost", port: "8080"}, "something.com:987[-789]": {host: "something.com", port: "987"}, "thisformatiswrong": {host: "", port: ""}, "somethingunix.sock[-876]": {host: "somethingunix.sock", port: ""}, "/var/dir/path/somethingunix.sock[-876]": {host: "/var/dir/path/somethingunix.sock", port: ""}, } for test, expected := range testCases { h, p := calcHostAndPort(test) if expected.host != h { t.Errorf("unexpected host - expected %s, got %s", expected.host, h) } if expected.port != p { t.Errorf("unexpected port - expected %s, got %s", expected.port, p) } } } func TestMonitor(t *testing.T) { var started, succeeded, failed bool origMonitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { started = true }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { succeeded = true }, Failed: func(ctx context.Context, e *event.CommandFailedEvent) { failed = true }, } nrMonitor := mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), origCommMon: origMonitor, } app := createTestApp() txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) nrMonitor.started(ctx, ste) if !started { t.Error("Original monitor not started") } if len(nrMonitor.segmentMap) != 1 { t.Errorf("Wrong number of segments, expected 1 but got %d", len(nrMonitor.segmentMap)) } seg, ok := nrMonitor.segmentMap[reqID] if !ok { t.Error("Segment not found in map") } confirmSegValues(t, seg) nrMonitor.succeeded(ctx, se) if !succeeded { t.Error("Original monitor not succeeded") } if len(nrMonitor.segmentMap) != 0 { t.Errorf("Wrong number of segments, expected 0 but got %d", len(nrMonitor.segmentMap)) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + thisHost + "/27017", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/commName", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/txnName", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "", Forced: false, Data: []float64{1.0}}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "OtherTransaction/Go/txnName", Forced: false, Data: []float64{1.0}}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/statement/MongoDB/collName/commName", "sampled": true, "category": "datastore", "component": "MongoDB", "span.kind": "client", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "peer.address": thisHost + ":27017", "peer.hostname": thisHost, "db.statement": "'commName' on 'collName' using 'MongoDB'", "db.instance": "testdb", "db.collection": "collName", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/txnName", "transaction.name": "OtherTransaction/Go/txnName", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) txn = app.StartTransaction("txnName") ctx = newrelic.NewContext(context.Background(), txn) nrMonitor.started(ctx, ste) if len(nrMonitor.segmentMap) != 1 { t.Errorf("Wrong number of segments, expected 1 but got %d", len(nrMonitor.segmentMap)) } seg, ok = nrMonitor.segmentMap[reqID] if !ok { t.Error("Segment not found in map") } confirmSegValues(t, seg) nrMonitor.failed(ctx, fe) if !failed { t.Error("failed not called") } if len(nrMonitor.segmentMap) != 0 { t.Errorf("Wrong number of segments, expected 0 but got %d", len(nrMonitor.segmentMap)) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + thisHost + "/27017", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/commName", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/txnName", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "", Forced: false, Data: []float64{2.0}}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "OtherTransaction/Go/txnName", Forced: false, Data: []float64{2.0}}, }) } func TestCollName(t *testing.T) { command := "find" ex1, _ := bson.Marshal(bson.D{{Key: command, Value: "numbers"}, {Key: "$db", Value: "testing"}}) ex2, _ := bson.Marshal(bson.D{{Key: "filter", Value: ""}}) testCases := map[string]bson.Raw{ "numbers": ex1, "": ex2, } for name, raw := range testCases { e := event.CommandStartedEvent{ Command: raw, CommandName: command, } result := collName(&e) if result != name { t.Errorf("Wrong collection name: %s", result) } } } func TestGetJsonQuery(t *testing.T) { // Test with a valid BSON document doc := bson.D{{Key: "foo", Value: "bar"}} result := getJsonQuery(doc) if len(result) == 0 { t.Error("Expected non-empty JSON for valid BSON document") } // Test with a value that cannot be marshaled type invalidType struct { Ch chan int } invalid := invalidType{Ch: make(chan int)} result = getJsonQuery(invalid) if string(result) != "" { t.Error("Expected empty JSON for value that cannot be marshaled") } } func confirmSegValues(t *testing.T, seg *newrelic.DatastoreSegment) { if seg.StartTime == (newrelic.SegmentStartTime{}) { t.Error("StartTime is zero value, expected it to be set") } if seg.Product != "MongoDB" { t.Errorf("Wrong product, expected 'MongoDB' but got '%s'", seg.Product) } if seg.Collection != "collName" { t.Errorf("Wrong collection name, expected 'collName' but got '%s'", seg.Collection) } if seg.Operation != "commName" { t.Errorf("Wrong operation name, expected 'commName' but got '%s'", seg.Operation) } if seg.ParameterizedQuery != "" { t.Errorf("Wrong parameterized query, expected '' but got '%s'", seg.ParameterizedQuery) } if seg.RawQuery != "" { t.Errorf("Wrong raw query, expected '' but got '%s'", seg.RawQuery) } if seg.QueryParameters != nil { t.Errorf("Wrong query parameters, expected nil but got '%v'", seg.QueryParameters) } if seg.Host != "localhost" { t.Errorf("Wrong host name, expected 'localhost' but got '%s'", seg.Host) } if seg.PortPathOrID != "27017" { t.Errorf("Wrong port, expected '27017' but got '%s'", seg.PortPathOrID) } if seg.DatabaseName != "testdb" { t.Errorf("Wrong database name, expected 'testdb' but got '%s'", seg.DatabaseName) } } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() } func TestWithRealMongoDB(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("TestTxn") ctx := newrelic.NewContext(context.Background(), txn) monitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { mongoMonitor := &mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), } mongoMonitor.started(ctx, e) }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { mongoMonitor := &mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), } mongoMonitor.succeeded(ctx, e) }, Failed: nil, } cmdMonitor := NewCommandMonitor(monitor) // Connect to real MongoDB t.Logf("Connecting to MongoDB at %s", MONGO_URI) client, err := mongo.Connect(options.Client().ApplyURI(MONGO_URI).SetMonitor(cmdMonitor)) if err != nil { t.Skipf("Skipping test, could not connect to MongoDB: %v", err) return } defer func(client *mongo.Client, ctx context.Context) { err := client.Disconnect(ctx) if err != nil { t.Fatalf("Failed to disconnect from MongoDB: %v", err) } }(client, ctx) // Perform an insert coll := client.Database(MONGO_DB).Collection("nrmongo_test") r, err := coll.InsertOne(ctx, bson.M{"foo": "bar"}) if err != nil { t.Fatalf("InsertOne failed: %v", err) } if r != nil { t.Log(r) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/TestTxn", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + MONGO_HOST + "/" + MONGO_PORT, Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/insert", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/TestTxn", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/nrmongo_test/insert", Scope: "", Forced: false, Data: []float64{1.0}}, {Name: "Datastore/statement/MongoDB/nrmongo_test/insert", Scope: "OtherTransaction/Go/TestTxn", Forced: false, Data: []float64{1.0}}, }) } go-agent-3.42.0/v3/integrations/nrmongo/000077500000000000000000000000001510742411500200165ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmongo/LICENSE.txt000066400000000000000000000264501510742411500216500ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrmongo/README.md000066400000000000000000000007001510742411500212720ustar00rootroot00000000000000# v3/integrations/nrmongo [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo) Package `nrmongo` instruments https://github.com/mongodb/mongo-go-driver ```go import "github.com/newrelic/go-agent/v3/integrations/nrmongo" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmongo). go-agent-3.42.0/v3/integrations/nrmongo/example/000077500000000000000000000000001510742411500214515ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmongo/example/main.go000066400000000000000000000031141510742411500227230ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrmongo" newrelic "github.com/newrelic/go-agent/v3/newrelic" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic Mongo App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } app.WaitForConnection(10 * time.Second) // If you have another CommandMonitor, you can pass it to NewCommandMonitor and it will get called along // with the NR monitor nrCmdMonitor := nrmongo.NewCommandMonitor(nil) ctx := context.Background() // nrCmdMonitor must be added after any other monitors are added, as previous options get overwritten. // This example assumes Mongo is running locally on port 27017 client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017").SetMonitor(nrCmdMonitor)) if err != nil { panic(err) } defer client.Disconnect(ctx) txn := app.StartTransaction("Mongo txn") // Make sure to add the newrelic.Transaction to the context nrCtx := newrelic.NewContext(context.Background(), txn) collection := client.Database("testing").Collection("numbers") _, err = collection.InsertOne(nrCtx, bson.M{"name": "exampleName", "value": "exampleValue"}) if err != nil { panic(err) } txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrmongo/go.mod000066400000000000000000000006001510742411500211200ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrmongo // As of Dec 2019, 1.10 is the mongo-driver requirement: // https://github.com/mongodb/mongo-go-driver#requirements go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 // mongo-driver does not support modules as of Nov 2019. go.mongodb.org/mongo-driver v1.17.4 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrmongo/nrmongo.go000066400000000000000000000133661510742411500220350ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrmongo instruments https://github.com/mongodb/mongo-go-driver // // Use this package to instrument your MongoDB calls without having to manually // create DatastoreSegments. To do so, first set the monitor in the connect // options using `SetMonitor` // (https://godoc.org/go.mongodb.org/mongo-driver/mongo/options#ClientOptions.SetMonitor): // // nrCmdMonitor := nrmongo.NewCommandMonitor(nil) // client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrCmdMonitor)) // // Note that it is important that this `nrmongo` monitor is the last monitor // set, otherwise it will be overwritten. If needing to use more than one // `event.CommandMonitor`, pass the original monitor to the // `nrmongo.NewCommandMonitor` function: // // origMon := &event.CommandMonitor{ // Started: origStarted, // Succeeded: origSucceeded, // Failed: origFailed, // } // nrCmdMonitor := nrmongo.NewCommandMonitor(origMon) // client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrCmdMonitor)) // // Then add the current transaction to the context used in any MongoDB call: // // ctx = newrelic.NewContext(context.Background(), txn) // collection := client.Database("testing").Collection("numbers") // resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) package nrmongo import ( "context" "regexp" "strings" "sync" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/event" ) func init() { internal.TrackUsage("integration", "datastore", "mongo") } type mongoMonitor struct { segmentMap map[int64]*newrelic.DatastoreSegment origCommMon *event.CommandMonitor sync.Mutex } // The Mongo connection ID is constructed as: `fmt.Sprintf("%s[-%d]", addr, nextConnectionID())`, // where addr is of the form `host:port` (or `a.sock` for unix sockets) // See https://github.com/mongodb/mongo-go-driver/blob/b39cd78ce7021252efee2fb44aa6e492d67680ef/x/mongo/driver/topology/connection.go#L68 // and https://github.com/mongodb/mongo-go-driver/blob/b39cd78ce7021252efee2fb44aa6e492d67680ef/x/mongo/driver/address/addr.go var connIDPattern = regexp.MustCompile(`([^:\[]+)(?::(\d+))?\[-\d+]`) // NewCommandMonitor returns a new `*event.CommandMonitor` // (https://godoc.org/go.mongodb.org/mongo-driver/event#CommandMonitor). If // provided, the original `*event.CommandMonitor` will be called as well. The // returned `*event.CommandMonitor` creates `newrelic.DatastoreSegment`s // (https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment) for each // database call. func NewCommandMonitor(original *event.CommandMonitor) *event.CommandMonitor { m := mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), origCommMon: original, } return &event.CommandMonitor{ Started: m.started, Succeeded: m.succeeded, Failed: m.failed, } } func (m *mongoMonitor) started(ctx context.Context, e *event.CommandStartedEvent) { var secureAgentEvent any if m.origCommMon != nil && m.origCommMon.Started != nil { m.origCommMon.Started(ctx, e) } txn := newrelic.FromContext(ctx) if txn == nil { return } if newrelic.IsSecurityAgentPresent() { commandName := e.CommandName if strings.ToLower(commandName) == "findandmodify" { value, ok := e.Command.Lookup("remove").BooleanOK() if ok && value { commandName = "delete" } } secureAgentEvent = newrelic.GetSecurityAgentInterface().SendEvent("MONGO", getJsonQuery(e.Command), commandName) } host, port := calcHostAndPort(e.ConnectionID) sgmt := newrelic.DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: newrelic.DatastoreMongoDB, Collection: collName(e), Operation: e.CommandName, Host: host, PortPathOrID: port, DatabaseName: e.DatabaseName, } if newrelic.IsSecurityAgentPresent() { sgmt.SetSecureAgentEvent(secureAgentEvent) } m.addSgmt(e, &sgmt) } func collName(e *event.CommandStartedEvent) string { coll := e.Command.Lookup(e.CommandName) collName, _ := coll.StringValueOK() return collName } func (m *mongoMonitor) addSgmt(e *event.CommandStartedEvent, sgmt *newrelic.DatastoreSegment) { m.Lock() defer m.Unlock() m.segmentMap[e.RequestID] = sgmt } func (m *mongoMonitor) succeeded(ctx context.Context, e *event.CommandSucceededEvent) { if sgmt := m.getSgmt(e.RequestID); sgmt != nil && newrelic.IsSecurityAgentPresent() { newrelic.GetSecurityAgentInterface().SendExitEvent(sgmt.GetSecureAgentEvent(), nil) } m.endSgmtIfExists(e.RequestID) if m.origCommMon != nil && m.origCommMon.Succeeded != nil { m.origCommMon.Succeeded(ctx, e) } } func (m *mongoMonitor) failed(ctx context.Context, e *event.CommandFailedEvent) { m.endSgmtIfExists(e.RequestID) if m.origCommMon != nil && m.origCommMon.Failed != nil { m.origCommMon.Failed(ctx, e) } } func (m *mongoMonitor) endSgmtIfExists(id int64) { m.getAndRemoveSgmt(id).End() } func (m *mongoMonitor) getAndRemoveSgmt(id int64) *newrelic.DatastoreSegment { m.Lock() defer m.Unlock() sgmt := m.segmentMap[id] if sgmt != nil { delete(m.segmentMap, id) } return sgmt } func (m *mongoMonitor) getSgmt(id int64) *newrelic.DatastoreSegment { m.Lock() defer m.Unlock() sgmt := m.segmentMap[id] return sgmt } func calcHostAndPort(connID string) (host string, port string) { // FindStringSubmatch either returns nil or an array of the size # of submatches + 1 (in this case 3) addressParts := connIDPattern.FindStringSubmatch(connID) if len(addressParts) == 3 { host = addressParts[1] port = addressParts[2] } return } func getJsonQuery(q interface{}) []byte { map_json, err := bson.MarshalExtJSON(q, true, true) if err != nil { return []byte("") } else { return map_json } } go-agent-3.42.0/v3/integrations/nrmongo/nrmongo_test.go000066400000000000000000000334331510742411500230710ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrmongo import ( "context" "go.mongodb.org/mongo-driver/bson/primitive" "os" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/sysinfo" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/event" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var ( MONGO_HOST = os.Getenv("MONGO_HOST") MONGO_PORT = os.Getenv("MONGO_PORT") MONGO_USER = os.Getenv("MONGO_INITDB_ROOT_USERNAME") MONGO_PASS = os.Getenv("MONGO_INITDB_ROOT_PASSWORD") MONGO_DB = os.Getenv("MONGO_DB") MONGO_URI = "mongodb://" + MONGO_USER + ":" + MONGO_PASS + "@" + MONGO_HOST + ":" + MONGO_PORT connID = "localhost:27017[-1]" reqID int64 = 10 raw, _ = bson.Marshal(bson.D{primitive.E{Key: "commName", Value: "collName"}, {Key: "$db", Value: "testing"}}) ste = &event.CommandStartedEvent{ Command: raw, DatabaseName: "testdb", CommandName: "commName", RequestID: reqID, ConnectionID: connID, } finishedEvent = event.CommandFinishedEvent{ Duration: 5, CommandName: "name", RequestID: reqID, ConnectionID: connID, } se = &event.CommandSucceededEvent{ CommandFinishedEvent: finishedEvent, Reply: nil, } fe = &event.CommandFailedEvent{ CommandFinishedEvent: finishedEvent, Failure: "failureCause", } thisHost, _ = sysinfo.Hostname() ) func TestOrigMonitorsAreCalled(t *testing.T) { var started, succeeded, failed bool origMonitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { started = true }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { succeeded = true }, Failed: func(ctx context.Context, e *event.CommandFailedEvent) { failed = true }, } ctx := context.Background() nrMonitor := NewCommandMonitor(origMonitor) nrMonitor.Started(ctx, ste) if !started { t.Error("started not called") } nrMonitor.Succeeded(ctx, se) if !succeeded { t.Error("succeeded not called") } nrMonitor.Failed(ctx, fe) if !failed { t.Error("failed not called") } } func TestClientOptsWithNullFunctions(t *testing.T) { origMonitor := &event.CommandMonitor{} // the monitor isn't nil, but its functions are. ctx := context.Background() nrMonitor := NewCommandMonitor(origMonitor) // Verifying no nil pointer dereferences nrMonitor.Started(ctx, ste) nrMonitor.Succeeded(ctx, se) nrMonitor.Failed(ctx, fe) } func TestHostAndPort(t *testing.T) { type hostAndPort struct { host string port string } testCases := map[string]hostAndPort{ "localhost:8080[-1]": {host: "localhost", port: "8080"}, "something.com:987[-789]": {host: "something.com", port: "987"}, "thisformatiswrong": {host: "", port: ""}, "somethingunix.sock[-876]": {host: "somethingunix.sock", port: ""}, "/var/dir/path/somethingunix.sock[-876]": {host: "/var/dir/path/somethingunix.sock", port: ""}, } for test, expected := range testCases { h, p := calcHostAndPort(test) if expected.host != h { t.Errorf("unexpected host - expected %s, got %s", expected.host, h) } if expected.port != p { t.Errorf("unexpected port - expected %s, got %s", expected.port, p) } } } func TestMonitor(t *testing.T) { var started, succeeded, failed bool origMonitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { started = true }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { succeeded = true }, Failed: func(ctx context.Context, e *event.CommandFailedEvent) { failed = true }, } nrMonitor := mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), origCommMon: origMonitor, } app := createTestApp() txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) nrMonitor.started(ctx, ste) if !started { t.Error("Original monitor not started") } if len(nrMonitor.segmentMap) != 1 { t.Errorf("Wrong number of segments, expected 1 but got %d", len(nrMonitor.segmentMap)) } seg, ok := nrMonitor.segmentMap[reqID] if !ok { t.Error("Segment not found in map") } confirmSegValues(t, seg) nrMonitor.succeeded(ctx, se) if !succeeded { t.Error("Original monitor not succeeded") } if len(nrMonitor.segmentMap) != 0 { t.Errorf("Wrong number of segments, expected 0 but got %d", len(nrMonitor.segmentMap)) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + thisHost + "/27017", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/commName", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/txnName", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "", Forced: false, Data: []float64{1.0}}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "OtherTransaction/Go/txnName", Forced: false, Data: []float64{1.0}}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/statement/MongoDB/collName/commName", "sampled": true, "category": "datastore", "component": "MongoDB", "span.kind": "client", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "peer.address": thisHost + ":27017", "peer.hostname": thisHost, "db.statement": "'commName' on 'collName' using 'MongoDB'", "db.instance": "testdb", "db.collection": "collName", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/txnName", "transaction.name": "OtherTransaction/Go/txnName", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) txn = app.StartTransaction("txnName") ctx = newrelic.NewContext(context.Background(), txn) nrMonitor.started(ctx, ste) if len(nrMonitor.segmentMap) != 1 { t.Errorf("Wrong number of segments, expected 1 but got %d", len(nrMonitor.segmentMap)) } seg, ok = nrMonitor.segmentMap[reqID] if !ok { t.Error("Segment not found in map") } confirmSegValues(t, seg) nrMonitor.failed(ctx, fe) if !failed { t.Error("failed not called") } if len(nrMonitor.segmentMap) != 0 { t.Errorf("Wrong number of segments, expected 0 but got %d", len(nrMonitor.segmentMap)) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/txnName", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + thisHost + "/27017", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/commName", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/txnName", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{2.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "", Forced: false, Data: []float64{2.0}}, {Name: "Datastore/statement/MongoDB/collName/commName", Scope: "OtherTransaction/Go/txnName", Forced: false, Data: []float64{2.0}}, }) } func TestCollName(t *testing.T) { command := "find" ex1, _ := bson.Marshal(bson.D{{Key: command, Value: "numbers"}, {Key: "$db", Value: "testing"}}) ex2, _ := bson.Marshal(bson.D{{Key: "filter", Value: ""}}) testCases := map[string]bson.Raw{ "numbers": ex1, "": ex2, } for name, raw := range testCases { e := event.CommandStartedEvent{ Command: raw, CommandName: command, } result := collName(&e) if result != name { t.Errorf("Wrong collection name: %s", result) } } } func TestGetJsonQuery(t *testing.T) { // Test with a valid BSON document doc := bson.D{{Key: "foo", Value: "bar"}} result := getJsonQuery(doc) if len(result) == 0 { t.Error("Expected non-empty JSON for valid BSON document") } // Test with a value that cannot be marshaled type invalidType struct { Ch chan int } invalid := invalidType{Ch: make(chan int)} result = getJsonQuery(invalid) if string(result) != "" { t.Error("Expected empty JSON for value that cannot be marshaled") } } func confirmSegValues(t *testing.T, seg *newrelic.DatastoreSegment) { if seg.StartTime == (newrelic.SegmentStartTime{}) { t.Error("StartTime is zero value, expected it to be set") } if seg.Product != "MongoDB" { t.Errorf("Wrong product, expected 'MongoDB' but got '%s'", seg.Product) } if seg.Collection != "collName" { t.Errorf("Wrong collection name, expected 'collName' but got '%s'", seg.Collection) } if seg.Operation != "commName" { t.Errorf("Wrong operation name, expected 'commName' but got '%s'", seg.Operation) } if seg.ParameterizedQuery != "" { t.Errorf("Wrong parameterized query, expected '' but got '%s'", seg.ParameterizedQuery) } if seg.RawQuery != "" { t.Errorf("Wrong raw query, expected '' but got '%s'", seg.RawQuery) } if seg.QueryParameters != nil { t.Errorf("Wrong query parameters, expected nil but got '%v'", seg.QueryParameters) } if seg.Host != "localhost" { t.Errorf("Wrong host name, expected 'localhost' but got '%s'", seg.Host) } if seg.PortPathOrID != "27017" { t.Errorf("Wrong port, expected '27017' but got '%s'", seg.PortPathOrID) } if seg.DatabaseName != "testdb" { t.Errorf("Wrong database name, expected 'testdb' but got '%s'", seg.DatabaseName) } } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() } func TestWithRealMongoDB(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction("TestTxn") ctx := newrelic.NewContext(context.Background(), txn) monitor := &event.CommandMonitor{ Started: func(ctx context.Context, e *event.CommandStartedEvent) { mongoMonitor := &mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), } mongoMonitor.started(ctx, e) }, Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { mongoMonitor := &mongoMonitor{ segmentMap: make(map[int64]*newrelic.DatastoreSegment), } mongoMonitor.succeeded(ctx, e) }, Failed: nil, } cmdMonitor := NewCommandMonitor(monitor) // Connect to real MongoDB t.Logf("Connecting to MongoDB at %s", MONGO_URI) client, err := mongo.Connect(ctx, options.Client().ApplyURI(MONGO_URI).SetMonitor(cmdMonitor)) if err != nil { t.Skipf("Skipping test, could not connect to MongoDB: %v", err) return } defer func(client *mongo.Client, ctx context.Context) { err := client.Disconnect(ctx) if err != nil { t.Fatalf("Failed to disconnect from MongoDB: %v", err) } }(client, ctx) // Perform an insert coll := client.Database(MONGO_DB).Collection("nrmongo_test") r, err := coll.InsertOne(ctx, bson.M{"foo": "bar"}) if err != nil { t.Fatalf("InsertOne failed: %v", err) } if r != nil { t.Log(r) } txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/TestTxn", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/instance/MongoDB/" + MONGO_HOST + "/" + MONGO_PORT, Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/MongoDB/insert", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/TestTxn", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/all", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "Datastore/MongoDB/allOther", Scope: "", Forced: true, Data: []float64{1.0}}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MongoDB/nrmongo_test/insert", Scope: "", Forced: false, Data: []float64{1.0}}, {Name: "Datastore/statement/MongoDB/nrmongo_test/insert", Scope: "OtherTransaction/Go/TestTxn", Forced: false, Data: []float64{1.0}}, }) } go-agent-3.42.0/v3/integrations/nrmssql/000077500000000000000000000000001510742411500200365ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmssql/LICENSE.txt000066400000000000000000000264501510742411500216700ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrmssql/README.md000066400000000000000000000005121510742411500213130ustar00rootroot00000000000000# v3/integrations/nrmssql [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql) Package `nrmssql` instruments github.com/microsoft/go-mssqldb. ```go import "github.com/newrelic/go-agent/v3/integrations/nrmssql" ``` go-agent-3.42.0/v3/integrations/nrmssql/example/000077500000000000000000000000001510742411500214715ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmssql/example/main.go000066400000000000000000000016241510742411500227470ustar00rootroot00000000000000package main import ( "context" "database/sql" "fmt" "os" "time" _ "github.com/newrelic/go-agent/v3/integrations/nrmssql" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // Set up a local ms sql docker db, err := sql.Open("nrmssql", "server=localhost;user id=sa;database=master;app name=MyAppName") if nil != err { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("MSSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("mssqlQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) from tables") var count int row.Scan(&count) txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of tables in information_schema", count) } go-agent-3.42.0/v3/integrations/nrmssql/go.mod000066400000000000000000000003271510742411500211460ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrmssql go 1.24 require ( github.com/microsoft/go-mssqldb v0.19.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrmssql/nrmssql.go000066400000000000000000000043401510742411500220650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrmssql instruments github.com/microsoft/go-mssqldb. // // Use this package to instrument your MSSQL calls without having to manually // create DatastoreSegments. This is done in a two step process: // // 1. Use this package's driver in place of the mssql driver. // // If your code is using sql.Open like this: // // import ( // _ "github.com/microsoft/go-mssqldb" // ) // // func main() { // db, err := sql.Open("mssql", "server=localhost;user id=sa;database=master;app name=MyAppName") // } // // Then change the side-effect import to this package, and open "nrmssql" instead: // // import ( // _ "github.com/newrelic/go-agent/v3/integrations/nrmssql" // ) // // func main() { // db, err := sql.Open("nrmssql", "server=localhost;user id=sa;database=master;app name=MyAppName") // } // // 2. Provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) from tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) from tables") package nrmssql import ( "database/sql" "fmt" mssql "github.com/microsoft/go-mssqldb" "github.com/microsoft/go-mssqldb/msdsn" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastoreMSSQL, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN, } ) func init() { sql.Register("nrmssql", newrelic.InstrumentSQLDriver(&mssql.Driver{}, baseBuilder)) internal.TrackUsage("integration", "driver", "mssql") } func parseDSN(s *newrelic.DatastoreSegment, dsn string) { cfg, err := msdsn.Parse(dsn) if nil != err { return } s.DatabaseName = cfg.Database s.Host = cfg.Host s.PortPathOrID = fmt.Sprint(cfg.Port) } go-agent-3.42.0/v3/integrations/nrmssql/nrmssql_test.go000066400000000000000000000035351510742411500231310ustar00rootroot00000000000000package nrmssql import ( "github.com/newrelic/go-agent/v3/newrelic" "testing" ) func TestParseDSN(t *testing.T) { testcases := []struct { dsn string expHost string expPortPathOrID string expDatabaseName string }{ // examples from https://github.com/denisenkom/go-mssqldb/blob/master/msdsn/conn_str_test.go { dsn: "server=server\\instance;database=testdb;user id=tester;password=pwd", expHost: "server", expPortPathOrID: "0", expDatabaseName: "testdb", }, { dsn: "server=(local)", expHost: "localhost", expPortPathOrID: "0", expDatabaseName: "", }, { dsn: "sqlserver://someuser@somehost?connection+timeout=30", expHost: "somehost", expPortPathOrID: "0", expDatabaseName: "", }, { dsn: "sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434?connection+timeout=30", expHost: "somehost", expPortPathOrID: "1434", expDatabaseName: "", }, { dsn: "Server=mssql.test.local; Initial Catalog=test; User ID=xxxxxxx; Password=abccxxxxxx;", expHost: "mssql.test.local", expPortPathOrID: "0", expDatabaseName: "test", }, { dsn: "sport=invalid", expHost: "localhost", expPortPathOrID: "0", expDatabaseName: "", }, } for _, test := range testcases { s := &newrelic.DatastoreSegment{} parseDSN(s, test.dsn) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } go-agent-3.42.0/v3/integrations/nrmysql/000077500000000000000000000000001510742411500200445ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmysql/LICENSE.txt000066400000000000000000000264501510742411500216760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrmysql/README.md000066400000000000000000000006751510742411500213330ustar00rootroot00000000000000# v3/integrations/nrmysql [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql) Package `nrmysql` instruments https://github.com/go-sql-driver/mysql. ```go import "github.com/newrelic/go-agent/v3/integrations/nrmysql" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql). go-agent-3.42.0/v3/integrations/nrmysql/example/000077500000000000000000000000001510742411500214775ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrmysql/example/main.go000066400000000000000000000020731510742411500227540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "database/sql" "fmt" "os" "time" _ "github.com/newrelic/go-agent/v3/integrations/nrmysql" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // Set up a local mysql docker container with: // docker run -it -p 3306:3306 --net "bridge" -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql db, err := sql.Open("nrmysql", "root@/information_schema") if nil != err { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("MySQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("mysqlQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) from tables") var count int row.Scan(&count) txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of tables in information_schema", count) } go-agent-3.42.0/v3/integrations/nrmysql/go.mod000066400000000000000000000005531510742411500211550ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrmysql // 1.10 is the Go version in mysql's go.mod go 1.24 require ( // v1.5.0 is the first mysql version to support gomod github.com/go-sql-driver/mysql v1.6.0 // v3.3.0 includes the new location of ParseQuery github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrmysql/nrmysql.go000066400000000000000000000054621510742411500221070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 // Package nrmysql instruments https://github.com/go-sql-driver/mysql. // // Use this package to instrument your MySQL calls without having to manually // create DatastoreSegments. This is done in a two step process: // // 1. Use this package's driver in place of the mysql driver. // // If your code is using sql.Open like this: // // import ( // _ "github.com/go-sql-driver/mysql" // ) // // func main() { // db, err := sql.Open("mysql", "user@unix(/path/to/socket)/dbname") // } // // Then change the side-effect import to this package, and open "nrmysql" instead: // // import ( // _ "github.com/newrelic/go-agent/v3/integrations/nrmysql" // ) // // func main() { // db, err := sql.Open("nrmysql", "user@unix(/path/to/socket)/dbname") // } // // 2. Provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) from tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) from tables") // // A working example is shown here: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrmysql/example/main.go package nrmysql import ( "crypto/tls" "database/sql" "net" "github.com/go-sql-driver/mysql" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastoreMySQL, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN, } ) func RegisterTLSConfig(key string, config *tls.Config) error { return mysql.RegisterTLSConfig(key, config) } func init() { sql.Register("nrmysql", newrelic.InstrumentSQLDriver(mysql.MySQLDriver{}, baseBuilder)) internal.TrackUsage("integration", "driver", "mysql") } func parseDSN(s *newrelic.DatastoreSegment, dsn string) { cfg, err := mysql.ParseDSN(dsn) if nil != err { return } parseConfig(s, cfg) } func parseConfig(s *newrelic.DatastoreSegment, cfg *mysql.Config) { s.DatabaseName = cfg.DBName var host, ppoid string switch cfg.Net { case "unix", "unixgram", "unixpacket": host = "localhost" ppoid = cfg.Addr case "cloudsql": host = cfg.Addr default: var err error host, ppoid, err = net.SplitHostPort(cfg.Addr) if nil != err { host = cfg.Addr } else if host == "" { host = "localhost" } } s.Host = host s.PortPathOrID = ppoid } go-agent-3.42.0/v3/integrations/nrmysql/nrmysql_test.go000066400000000000000000000115561510742411500231470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrmysql import ( "testing" "github.com/go-sql-driver/mysql" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestParseDSN(t *testing.T) { testcases := []struct { dsn string expHost string expPortPathOrID string expDatabaseName string }{ // examples from https://github.com/go-sql-driver/mysql README { dsn: "user@unix(/path/to/socket)/dbname", expHost: "localhost", expPortPathOrID: "/path/to/socket", expDatabaseName: "dbname", }, { dsn: "root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local", expHost: "localhost", expPortPathOrID: "/tmp/mysql.sock", expDatabaseName: "myDatabase", }, { dsn: "user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true", expHost: "localhost", expPortPathOrID: "5555", expDatabaseName: "dbname", }, { dsn: "user:password@/dbname?sql_mode=TRADITIONAL", expHost: "127.0.0.1", expPortPathOrID: "3306", expDatabaseName: "dbname", }, { dsn: "user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci", expHost: "de:ad:be:ef::ca:fe", expPortPathOrID: "80", expDatabaseName: "dbname", }, { dsn: "id:password@tcp(your-amazonaws-uri.com:3306)/dbname", expHost: "your-amazonaws-uri.com", expPortPathOrID: "3306", expDatabaseName: "dbname", }, { dsn: "user@cloudsql(project-id:instance-name)/dbname", expHost: "project-id:instance-name", expPortPathOrID: "", expDatabaseName: "dbname", }, { dsn: "user@cloudsql(project-id:regionname:instance-name)/dbname", expHost: "project-id:regionname:instance-name", expPortPathOrID: "", expDatabaseName: "dbname", }, { dsn: "user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped", expHost: "127.0.0.1", expPortPathOrID: "3306", expDatabaseName: "dbname", }, { dsn: "user:password@/dbname", expHost: "127.0.0.1", expPortPathOrID: "3306", expDatabaseName: "dbname", }, { dsn: "user:password@/", expHost: "127.0.0.1", expPortPathOrID: "3306", expDatabaseName: "", }, { dsn: "this is not a dsn", expHost: "", expPortPathOrID: "", expDatabaseName: "", }, } for _, test := range testcases { s := &newrelic.DatastoreSegment{} parseDSN(s, test.dsn) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } func TestParseConfig(t *testing.T) { testcases := []struct { cfgNet string cfgAddr string cfgDBName string expHost string expPortPathOrID string expDatabaseName string }{ { cfgDBName: "mydb", expDatabaseName: "mydb", }, { cfgNet: "unixgram", cfgAddr: "/path/to/my/sock", expHost: "localhost", expPortPathOrID: "/path/to/my/sock", }, { cfgNet: "unixpacket", cfgAddr: "/path/to/my/sock", expHost: "localhost", expPortPathOrID: "/path/to/my/sock", }, { cfgNet: "udp", cfgAddr: "[fe80::1%lo0]:53", expHost: "fe80::1%lo0", expPortPathOrID: "53", }, { cfgNet: "tcp", cfgAddr: ":80", expHost: "localhost", expPortPathOrID: "80", }, { cfgNet: "ip4:1", cfgAddr: "192.0.2.1", expHost: "192.0.2.1", expPortPathOrID: "", }, { cfgNet: "tcp6", cfgAddr: "golang.org:http", expHost: "golang.org", expPortPathOrID: "http", }, { cfgNet: "ip6:ipv6-icmp", cfgAddr: "2001:db8::1", expHost: "2001:db8::1", expPortPathOrID: "", }, } for _, test := range testcases { s := &newrelic.DatastoreSegment{} cfg := &mysql.Config{ Net: test.cfgNet, Addr: test.cfgAddr, DBName: test.cfgDBName, } parseConfig(s, cfg) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } go-agent-3.42.0/v3/integrations/nrnats/000077500000000000000000000000001510742411500176445ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrnats/LICENSE.txt000066400000000000000000000264501510742411500214760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrnats/README.md000066400000000000000000000006631510742411500211300ustar00rootroot00000000000000# v3/integrations/nrnats [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats) Package `nrnats` instruments https://github.com/nats-io/nats.go. ```go import "github.com/newrelic/go-agent/v3/integrations/nrnats" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats). go-agent-3.42.0/v3/integrations/nrnats/example_test.go000066400000000000000000000021261510742411500226660ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrnats_test import ( "fmt" "time" "github.com/nats-io/nats.go" "github.com/newrelic/go-agent/v3/integrations/nrnats" "github.com/newrelic/go-agent/v3/newrelic" ) func currentTransaction() *newrelic.Transaction { return nil } func ExampleStartPublishSegment() { nc, _ := nats.Connect(nats.DefaultURL) txn := currentTransaction() subject := "testing.subject" // Start the Publish segment seg := nrnats.StartPublishSegment(txn, nc, subject) err := nc.Publish(subject, []byte("Hello World")) if nil != err { panic(err) } // Manually end the segment seg.End() } func ExampleStartPublishSegment_defer() { nc, _ := nats.Connect(nats.DefaultURL) txn := currentTransaction() subject := "testing.subject" // Start the Publish segment and defer End till the func returns defer nrnats.StartPublishSegment(txn, nc, subject).End() m, err := nc.Request(subject, []byte("request"), time.Second) if nil != err { panic(err) } fmt.Println("Received reply message:", string(m.Data)) } go-agent-3.42.0/v3/integrations/nrnats/examples/000077500000000000000000000000001510742411500214625ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrnats/examples/README.md000066400000000000000000000005471510742411500227470ustar00rootroot00000000000000# Example NATS app In this example app you can find several different ways of instrumenting NATS functions using New Relic. In order to run the app, make sure the following assumptions are correct: * Your New Relic license key is available as an environment variable named `NEW_RELIC_LICENSE_KEY` * A NATS server is running locally at the `nats.DefaultURL` go-agent-3.42.0/v3/integrations/nrnats/examples/main.go000066400000000000000000000114461510742411500227430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "sync" "time" nats "github.com/nats-io/nats.go" "github.com/newrelic/go-agent/v3/integrations/nrnats" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) var app *newrelic.Application func doAsync(nc *nats.Conn, txn *newrelic.Transaction) { wg := sync.WaitGroup{} subj := "async" // Simple Async Subscriber // Use the nrnats.SubWrapper to wrap the nats.MsgHandler and create a // newrelic.Transaction with each processed nats.Msg _, err := nc.Subscribe(subj, nrnats.SubWrapper(app, func(m *nats.Msg) { defer wg.Done() fmt.Println("Received async message:", string(m.Data)) })) if nil != err { panic(err) } // Simple Publisher wg.Add(1) // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Publish seg := nrnats.StartPublishSegment(txn, nc, subj) err = nc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } wg.Wait() } func doQueue(nc *nats.Conn, txn *newrelic.Transaction) { wg := sync.WaitGroup{} subj := "queue" // Queue Subscriber // Use the nrnats.SubWrapper to wrap the nats.MsgHandler and create a // newrelic.Transaction with each processed nats.Msg _, err := nc.QueueSubscribe(subj, "myQueueName", nrnats.SubWrapper(app, func(m *nats.Msg) { defer wg.Done() fmt.Println("Received queue message:", string(m.Data)) })) if nil != err { panic(err) } wg.Add(1) // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Publish seg := nrnats.StartPublishSegment(txn, nc, subj) err = nc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } wg.Wait() } func doSync(nc *nats.Conn, txn *newrelic.Transaction) { subj := "sync" // Simple Sync Subscriber sub, err := nc.SubscribeSync(subj) if nil != err { panic(err) } // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Publish seg := nrnats.StartPublishSegment(txn, nc, subj) err = nc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } m, err := sub.NextMsg(time.Second) if nil != err { panic(err) } fmt.Println("Received sync message:", string(m.Data)) } func doChan(nc *nats.Conn, txn *newrelic.Transaction) { subj := "chan" // Channel Subscriber ch := make(chan *nats.Msg) _, err := nc.ChanSubscribe(subj, ch) if nil != err { panic(err) } // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Publish seg := nrnats.StartPublishSegment(txn, nc, subj) err = nc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } m := <-ch fmt.Println("Received chan message:", string(m.Data)) } func doReply(nc *nats.Conn, txn *newrelic.Transaction) { subj := "reply" // Replies nc.Subscribe(subj, func(m *nats.Msg) { // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Publish seg := nrnats.StartPublishSegment(txn, nc, m.Reply) nc.Publish(m.Reply, []byte("Hello World")) seg.End() }) // Requests // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Request seg := nrnats.StartPublishSegment(txn, nc, subj) m, err := nc.Request(subj, []byte("request"), time.Second) seg.End() if nil != err { panic(err) } fmt.Println("Received reply message:", string(m.Data)) } func doRespond(nc *nats.Conn, txn *newrelic.Transaction) { subj := "respond" // Respond nc.Subscribe(subj, func(m *nats.Msg) { // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to m.Respond seg := nrnats.StartPublishSegment(txn, nc, m.Reply) m.Respond([]byte("Hello World")) seg.End() }) // Requests // Use nrnats.StartPublishSegment to create a // newrelic.MessageProducerSegment for the call to nc.Request seg := nrnats.StartPublishSegment(txn, nc, subj) m, err := nc.Request(subj, []byte("request"), time.Second) seg.End() if nil != err { panic(err) } fmt.Println("Received respond message:", string(m.Data)) } func main() { // Initialize agent var err error app, err = newrelic.NewApplication( newrelic.ConfigAppName("NATS App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } defer app.Shutdown(10 * time.Second) err = app.WaitForConnection(5 * time.Second) if nil != err { panic(err) } txn := app.StartTransaction("main") defer txn.End() // Connect to a server nc, err := nats.Connect(nats.DefaultURL) if nil != err { panic(err) } defer nc.Drain() doAsync(nc, txn) doQueue(nc, txn) doSync(nc, txn) doChan(nc, txn) doReply(nc, txn) doRespond(nc, txn) } go-agent-3.42.0/v3/integrations/nrnats/go.mod000066400000000000000000000006201510742411500207500ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrnats // As of Jun 2023, 1.19 is the earliest version of Go tested by nats: // https://github.com/nats-io/nats.go/blob/master/.travis.yml go 1.24 toolchain go1.23.4 require ( github.com/nats-io/nats-server v1.4.1 github.com/nats-io/nats.go v1.36.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrnats/nrnats.go000066400000000000000000000051261510742411500215040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrnats import ( "strings" nats "github.com/nats-io/nats.go" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) // StartPublishSegment creates and starts a `newrelic.MessageProducerSegment` // (https://godoc.org/github.com/newrelic/go-agent#MessageProducerSegment) for NATS // publishers. Call this function before calling any method that publishes or // responds to a NATS message. Call `End()` // (https://godoc.org/github.com/newrelic/go-agent#MessageProducerSegment.End) on the // returned newrelic.MessageProducerSegment when the publish is complete. The // `newrelic.Transaction` and `nats.Conn` parameters are required. The subject // parameter is the subject of the publish call and is used in metric and span // names. func StartPublishSegment(txn *newrelic.Transaction, nc *nats.Conn, subject string) *newrelic.MessageProducerSegment { if nil == txn { return nil } if nil == nc { return nil } return &newrelic.MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "NATS", DestinationType: newrelic.MessageTopic, DestinationName: subject, DestinationTemporary: strings.HasPrefix(subject, "_INBOX"), } } // SubWrapper can be used to wrap the function for nats.Subscribe (https://godoc.org/github.com/nats-io/go-nats#Conn.Subscribe // or https://godoc.org/github.com/nats-io/go-nats#EncodedConn.Subscribe) // and nats.QueueSubscribe (https://godoc.org/github.com/nats-io/go-nats#Conn.QueueSubscribe or // https://godoc.org/github.com/nats-io/go-nats#EncodedConn.QueueSubscribe) // If the `newrelic.Application` parameter is non-nil, it will create a `newrelic.Transaction` and end the transaction // when the passed function is complete. func SubWrapper(app *newrelic.Application, f func(msg *nats.Msg)) func(msg *nats.Msg) { if app == nil { return f } return func(msg *nats.Msg) { namer := internal.MessageMetricKey{ Library: "NATS", DestinationType: string(newrelic.MessageTopic), DestinationName: msg.Subject, Consumer: true, } txn := app.StartTransaction(namer.Name()) defer txn.End() integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageRoutingKey, msg.Sub.Subject, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageQueueName, msg.Sub.Queue, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageReplyTo, msg.Reply, nil) f(msg) } } go-agent-3.42.0/v3/integrations/nrnats/nrnats_doc.go000066400000000000000000000053231510742411500223300ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrnats instruments https://github.com/nats-io/nats.go. // // This package can be used to simplify instrumenting NATS publishers and subscribers. Currently due to the nature of // the NATS framework we are limited to two integration points: `StartPublishSegment` for publishers, and `SubWrapper` // for subscribers. // // # NATS publishers // // To generate an external segment for any method that publishes or responds to a NATS message, use the // `StartPublishSegment` method. The resulting segment will also need to be ended. Example: // // nc, _ := nats.Connect(nats.DefaultURL) // txn := currentTransaction() // current newrelic.Transaction // subject := "testing.subject" // seg := nrnats.StartPublishSegment(txn, nc, subject) // err := nc.Publish(subject, []byte("Hello World")) // if nil != err { // panic(err) // } // seg.End() // // Or: // // nc, _ := nats.Connect(nats.DefaultURL) // txn := currentTransaction() // current newrelic.Transaction // subject := "testing.subject" // defer nrnats.StartPublishSegment(txn, nc, subject).End() // nc.Publish(subject, []byte("Hello World")) // // StartPublishSegment can be used with a NATS Streamming Connection as well // (https://github.com/nats-io/stan.go). Use the `NatsConn()` method on the // `stan.Conn` interface (https://godoc.org/github.com/nats-io/stan#Conn) to // access the `nats.Conn` object. // // sc, _ := stan.Connect(clusterID, clientID) // txn := currentTransaction() // subject := "testing.subject" // defer nrnats.StartPublishSegment(txn, sc.NatsConn(), subject).End() // sc.Publish(subject, []byte("Hello World")) // // # NATS subscribers // // The `nrnats.SubWrapper` function can be used to wrap the function for `nats.Subscribe` // (https://godoc.org/github.com/nats-io/go-nats#Conn.Subscribe or // https://godoc.org/github.com/nats-io/go-nats#EncodedConn.Subscribe) // and `nats.QueueSubscribe` (https://godoc.org/github.com/nats-io/go-nats#Conn.QueueSubscribe or // https://godoc.org/github.com/nats-io/go-nats#EncodedConn.QueueSubscribe) // If the `newrelic.Application` parameter is non-nil, it will create a `newrelic.Transaction` and end the transaction // when the passed function is complete. Example: // // nc, _ := nats.Connect(nats.DefaultURL) // app := createNRApp() // newrelic.Application // subject := "testing.subject" // nc.Subscribe(subject, nrnats.SubWrapper(app, myMessageHandler)) // // Full Publisher/Subscriber example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrnats/examples/main.go package nrnats import "github.com/newrelic/go-agent/v3/internal" func init() { internal.TrackUsage("integration", "framework", "nats") } go-agent-3.42.0/v3/integrations/nrnats/nrnats_test.go000066400000000000000000000173671510742411500225550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrnats import ( "os" "sync" "testing" "time" "github.com/nats-io/nats-server/test" nats "github.com/nats-io/nats.go" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func TestMain(m *testing.M) { s := test.RunDefaultServer() defer s.Shutdown() os.Exit(m.Run()) } func testApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var cfgFn = func(cfg *newrelic.Config) { cfg.Attributes.Include = append(cfg.Attributes.Include, newrelic.AttributeMessageRoutingKey, newrelic.AttributeMessageQueueName, newrelic.AttributeMessageExchangeType, newrelic.AttributeMessageReplyTo, newrelic.AttributeMessageCorrelationID, ) } func TestStartPublishSegmentNilTxn(t *testing.T) { // Make sure that a nil transaction does not cause panics nc, err := nats.Connect(nats.DefaultURL) if nil != err { t.Fatal(err) } defer nc.Close() StartPublishSegment(nil, nc, "mysubject").End() } func TestStartPublishSegmentNilConn(t *testing.T) { // Make sure that a nil nats.Conn does not cause panics and does not record // metrics app := testApp() txn := app.StartTransaction("testing") StartPublishSegment(txn, nil, "mysubject").End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/testing", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/testing", Scope: "", Forced: false, Data: nil}, }) } func TestStartPublishSegmentBasic(t *testing.T) { app := testApp() txn := app.StartTransaction("testing") nc, err := nats.Connect(nats.DefaultURL) if nil != err { t.Fatal(err) } defer nc.Close() StartPublishSegment(txn, nc, "mysubject").End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/NATS/Topic/Produce/Named/mysubject", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/NATS/Topic/Produce/Named/mysubject", Scope: "OtherTransaction/Go/testing", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/testing", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/testing", Scope: "", Forced: false, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "generic", "name": "MessageBroker/NATS/Topic/Produce/Named/mysubject", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/testing", "transaction.name": "OtherTransaction/Go/testing", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/testing", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/testing", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "MessageBroker/NATS/Topic/Produce/Named/mysubject", Attributes: map[string]interface{}{}, }, }, }}, }, }, }) } func TestSubWrapperWithNilApp(t *testing.T) { nc, err := nats.Connect(nats.DefaultURL) if err != nil { t.Fatal("Error connecting to NATS server", err) } wg := sync.WaitGroup{} nc.Subscribe("subject1", SubWrapper(nil, func(msg *nats.Msg) { wg.Done() })) wg.Add(1) nc.Publish("subject1", []byte("data")) wg.Wait() } func TestSubWrapper(t *testing.T) { nc, err := nats.Connect(nats.DefaultURL) if err != nil { t.Fatal("Error connecting to NATS server", err) } wg := sync.WaitGroup{} app := testApp() nc.QueueSubscribe("subject2", "queue1", WgWrapper(&wg, SubWrapper(app.Application, func(msg *nats.Msg) {}))) wg.Add(1) nc.Request("subject2", []byte("data"), time.Second) wg.Wait() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/Message/NATS/Topic/Named/subject2", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/Message/NATS/Topic/Named/subject2", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/Message/NATS/Topic/Named/subject2", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "message.replyTo": internal.MatchAnything, // starts with _INBOX "message.routingKey": "subject2", "message.queueName": "queue1", }, UserAttributes: map[string]interface{}{}, }, }) } func TestStartPublishSegmentNaming(t *testing.T) { testCases := []struct { subject string metric string }{ {subject: "", metric: "MessageBroker/NATS/Topic/Produce/Named/Unknown"}, {subject: "mysubject", metric: "MessageBroker/NATS/Topic/Produce/Named/mysubject"}, {subject: "_INBOX.asldfkjsldfjskd.ldskfjls", metric: "MessageBroker/NATS/Topic/Produce/Temp"}, } nc, err := nats.Connect(nats.DefaultURL) if nil != err { t.Fatal(err) } defer nc.Close() for _, tc := range testCases { app := testApp() txn := app.StartTransaction("testing") StartPublishSegment(txn, nc, tc.subject).End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/testing", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/testing", Scope: "", Forced: false, Data: nil}, {Name: tc.metric, Scope: "", Forced: false, Data: nil}, {Name: tc.metric, Scope: "OtherTransaction/Go/testing", Forced: false, Data: nil}, }) } } // Wrapper function to ensure that the NR wrapper is done recording transaction data before wg.Done() is called func WgWrapper(wg *sync.WaitGroup, nrWrap func(msg *nats.Msg)) func(msg *nats.Msg) { return func(msg *nats.Msg) { nrWrap(msg) wg.Done() } } go-agent-3.42.0/v3/integrations/nrnats/test/000077500000000000000000000000001510742411500206235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrnats/test/LICENSE.txt000066400000000000000000000264501510742411500224550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrnats/test/go.mod000066400000000000000000000007071510742411500217350ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/test // This module exists to avoid having extra nrnats module dependencies. go 1.24 replace github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 => ../ require ( github.com/nats-io/nats-server v1.4.1 github.com/nats-io/nats.go v1.36.0 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 ) replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/nropenai/000077500000000000000000000000001510742411500201525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/LICENSE.txt000066400000000000000000000264501510742411500220040ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nropenai/examples/000077500000000000000000000000001510742411500217705ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletion/000077500000000000000000000000001510742411500250015ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletion/chatcompletion_example.go000066400000000000000000000051351510742411500320600ustar00rootroot00000000000000package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nropenai" "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkoukk/tiktoken-go" openai "github.com/sashabaranov/go-openai" ) func main() { // Start New Relic Application app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic OpenAI App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), // Enable AI Monitoring // NOTE - If High Security Mode is enabled, AI Monitoring will always be disabled newrelic.ConfigAIMonitoringEnabled(true), ) if nil != err { panic(err) } app.WaitForConnection(10 * time.Second) // SetLLMTokenCountCallback allows for custom token counting, if left unset and if newrelic.ConfigAIMonitoringRecordContentEnabled() // is disabled, no token counts will be reported app.SetLLMTokenCountCallback(func(modelName string, content string) int { var tokensPerMessage, tokensPerName int switch modelName { case "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613": tokensPerMessage = 3 tokensPerName = 1 case "gpt-3.5-turbo-0301": tokensPerMessage = 4 tokensPerName = -1 } tkm, err := tiktoken.EncodingForModel(modelName) if err != nil { fmt.Println("error getting tokens", err) return 0 } token := tkm.Encode(content, nil, nil) totalTokens := len(token) + tokensPerMessage + tokensPerName return totalTokens }) // OpenAI Config - Additionally, NRDefaultAzureConfig(apiKey, baseURL string) can be used for Azure cfg := nropenai.NRDefaultConfig(os.Getenv("OPEN_AI_API_KEY")) // Create OpenAI Client - Additionally, NRNewClient(authToken string) can be used client := nropenai.NRNewClientWithConfig(cfg) // Add any custom attributes // NOTE: Attributes must start with "llm.", otherwise they will be ignored client.AddCustomAttributes(map[string]interface{}{ "llm.foo": "bar", "llm.pi": 3.14, }) // GPT Request req := openai.ChatCompletionRequest{ Model: openai.GPT4, Temperature: 0.7, MaxTokens: 150, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "What is Observability in Software Engineering?", }, }, } // NRCreateChatCompletion returns a wrapped version of openai.ChatCompletionResponse resp, err := nropenai.NRCreateChatCompletion(client, req, app) if err != nil { panic(err) } if len(resp.ChatCompletionResponse.Choices) == 0 { fmt.Println("No choices returned") } // Shutdown Application app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletionfeedback/000077500000000000000000000000001510742411500264465ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletionfeedback/chatcompletionfeedback.go000066400000000000000000000037531510742411500334630ustar00rootroot00000000000000package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nropenai" "github.com/newrelic/go-agent/v3/newrelic" openai "github.com/sashabaranov/go-openai" ) // Simulates feedback being sent to New Relic. Feedback on a chat completion requires // having access to the ChatCompletionResponseWrapper which is returned by the NRCreateChatCompletion function. func SendFeedback(app *newrelic.Application, resp nropenai.ChatCompletionResponseWrapper) { trace_id := resp.TraceID rating := "5" category := "informative" message := "The response was concise yet thorough." customMetadata := map[string]interface{}{ "foo": "bar", "pi": 3.14, } app.RecordLLMFeedbackEvent(trace_id, rating, category, message, customMetadata) } func main() { // Start New Relic Application app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic OpenAI App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigAIMonitoringEnabled(true), ) if nil != err { panic(err) } app.WaitForConnection(10 * time.Second) // OpenAI Config - Additionally, NRDefaultAzureConfig(apiKey, baseURL string) can be used for Azure cfg := nropenai.NRDefaultConfig(os.Getenv("OPEN_AI_API_KEY")) client := nropenai.NRNewClientWithConfig(cfg) // GPT Request req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0.7, MaxTokens: 150, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "What is observability in software engineering?", }, }, } // NRCreateChatCompletion returns a wrapped version of openai.ChatCompletionResponse resp, err := nropenai.NRCreateChatCompletion(client, req, app) if err != nil { panic(err) } // Print the contents of the message fmt.Println("Message Response: ", resp.ChatCompletionResponse.Choices[0].Message.Content) SendFeedback(app, resp) // Shutdown Application app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletionstreaming/000077500000000000000000000000001510742411500267135ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/examples/chatcompletionstreaming/chatcompletionstreaming.go000066400000000000000000000066571510742411500342030ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nropenai" "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkoukk/tiktoken-go" openai "github.com/sashabaranov/go-openai" ) // Simulates feedback being sent to New Relic. Feedback on a chat completion requires // having access to the ChatCompletionResponseWrapper which is returned by the NRCreateChatCompletion function. func SendFeedback(app *newrelic.Application, resp nropenai.ChatCompletionStreamWrapper) { trace_id := resp.TraceID rating := "5" category := "informative" message := "The response was concise yet thorough." customMetadata := map[string]interface{}{ "foo": "bar", "pi": 3.14, } app.RecordLLMFeedbackEvent(trace_id, rating, category, message, customMetadata) } func main() { // Start New Relic Application app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic OpenAI App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), // Enable AI Monitoring // NOTE - If High Security Mode is enabled, AI Monitoring will always be disabled newrelic.ConfigAIMonitoringEnabled(true), ) if nil != err { panic(err) } app.WaitForConnection(10 * time.Second) // SetLLMTokenCountCallback allows for custom token counting, if left unset and if newrelic.ConfigAIMonitoringRecordContentEnabled() // is disabled, no token counts will be reported app.SetLLMTokenCountCallback(func(modelName string, content string) int { var tokensPerMessage, tokensPerName int switch modelName { case "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613": tokensPerMessage = 3 tokensPerName = 1 case "gpt-3.5-turbo-0301": tokensPerMessage = 4 tokensPerName = -1 } tkm, err := tiktoken.EncodingForModel(modelName) if err != nil { fmt.Println("error getting tokens", err) return 0 } token := tkm.Encode(content, nil, nil) totalTokens := len(token) + tokensPerMessage + tokensPerName return totalTokens }) // OpenAI Config - Additionally, NRDefaultAzureConfig(apiKey, baseURL string) can be used for Azure cfg := nropenai.NRDefaultConfig(os.Getenv("OPEN_AI_API_KEY")) // Create OpenAI Client - Additionally, NRNewClient(authToken string) can be used client := nropenai.NRNewClientWithConfig(cfg) // Add any custom attributes // NOTE: Attributes must start with "llm.", otherwise they will be ignored client.AddCustomAttributes(map[string]interface{}{ "llm.foo": "bar", "llm.pi": 3.14, }) // GPT Request req := openai.ChatCompletionRequest{ Model: openai.GPT4, Temperature: 0.7, MaxTokens: 1500, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "What is observability in software engineering?", }, }, Stream: true, } ctx := context.Background() stream, err := nropenai.NRCreateChatCompletionStream(client, ctx, req, app) if err != nil { panic(err) } fmt.Printf("Stream response: ") for { var response openai.ChatCompletionStreamResponse response, err = stream.Recv() if errors.Is(err, io.EOF) { fmt.Println("\nStream finished") break } if err != nil { fmt.Printf("\nStream error: %v\n", err) return } fmt.Print(response.Choices[0].Delta.Content) } stream.Close() SendFeedback(app, *stream) // Shutdown Application app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nropenai/examples/embeddings/000077500000000000000000000000001510742411500240715ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nropenai/examples/embeddings/embeddings_example.go000066400000000000000000000044051510742411500302370ustar00rootroot00000000000000package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nropenai" "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkoukk/tiktoken-go" openai "github.com/sashabaranov/go-openai" ) func main() { // Start New Relic Application app, err := newrelic.NewApplication( newrelic.ConfigAppName("Basic OpenAI App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), // Enable AI Monitoring newrelic.ConfigAIMonitoringEnabled(true), ) if nil != err { panic(err) } app.WaitForConnection(10 * time.Second) app.SetLLMTokenCountCallback(func(modelName string, content string) int { var tokensPerMessage, tokensPerName int switch modelName { case "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613": tokensPerMessage = 3 tokensPerName = 1 case "gpt-3.5-turbo-0301": tokensPerMessage = 4 tokensPerName = -1 } tkm, err := tiktoken.EncodingForModel(modelName) if err != nil { fmt.Println("error getting tokens", err) return 0 } token := tkm.Encode(content, nil, nil) totalTokens := len(token) + tokensPerMessage + tokensPerName return totalTokens }) // OpenAI Config - Additionally, NRDefaultAzureConfig(apiKey, baseURL string) can be used for Azure cfg := nropenai.NRDefaultConfig(os.Getenv("OPEN_AI_API_KEY")) // Create OpenAI Client - Additionally, NRNewClient(authToken string) can be used client := nropenai.NRNewClientWithConfig(cfg) // Add any custom attributes // NOTE: Attributes must start with "llm.", otherwise they will be ignored client.CustomAttributes = map[string]interface{}{ "llm.foo": "bar", "llm.pi": 3.14, } fmt.Println("Creating Embedding Request...") // Create Embeddings embeddingReq := openai.EmbeddingRequest{ Input: []string{ "The food was delicious and the waiter", "Other examples of embedding request", }, Model: openai.AdaEmbeddingV2, EncodingFormat: openai.EmbeddingEncodingFormatFloat, } resp, err := nropenai.NRCreateEmbedding(client, embeddingReq, app) if err != nil { panic(err) } fmt.Println("Embedding Created!") fmt.Println(resp.Usage.PromptTokens) // Shutdown Application app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nropenai/go.mod000066400000000000000000000004371510742411500212640ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nropenai go 1.24 require ( github.com/google/uuid v1.6.0 github.com/newrelic/go-agent/v3 v3.42.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/sashabaranov/go-openai v1.20.2 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nropenai/nropenai.go000066400000000000000000000623121510742411500223200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nropenai import ( "context" "errors" "reflect" "runtime/debug" "strings" "sync" "time" "github.com/google/uuid" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/sashabaranov/go-openai" ) var reportStreamingDisabled func() func init() { reportStreamingDisabled = sync.OnceFunc(func() { internal.TrackUsage("Go", "ML", "Streaming", "Disabled") }) // Get current go-openai version info, ok := debug.ReadBuildInfo() if info != nil && ok { for _, module := range info.Deps { if module != nil && strings.Contains(module.Path, "go-openai") { internal.TrackUsage("Go", "ML", "OpenAI", module.Version) return } } } internal.TrackUsage("Go", "ML", "OpenAI", "unknown") } var ( errAIMonitoringDisabled = errors.New("AI Monitoring is set to disabled or High Security Mode is enabled. Please enable AI Monitoring and ensure High Security Mode is disabled") ) // OpenAIClient is any type that can invoke OpenAI model with a request. type OpenAIClient interface { CreateChatCompletion(ctx context.Context, request openai.ChatCompletionRequest) (response openai.ChatCompletionResponse, err error) CreateChatCompletionStream(ctx context.Context, request openai.ChatCompletionRequest) (stream *openai.ChatCompletionStream, err error) CreateEmbeddings(ctx context.Context, conv openai.EmbeddingRequestConverter) (res openai.EmbeddingResponse, err error) } // Wrapper for OpenAI Configuration type ConfigWrapper struct { Config *openai.ClientConfig } // Wrapper for OpenAI Client with Custom Attributes that can be set for all LLM Events type ClientWrapper struct { Client OpenAIClient // Set of Custom Attributes that get tied to all LLM Events CustomAttributes map[string]interface{} } // Wrapper for ChatCompletionResponse that is returned from NRCreateChatCompletion. It also includes the TraceID of the transaction for linking a chat response with it's feedback type ChatCompletionResponseWrapper struct { ChatCompletionResponse openai.ChatCompletionResponse TraceID string } // Wrapper for ChatCompletionStream that is returned from NRCreateChatCompletionStream // Contains attributes that get populated during the streaming process type ChatCompletionStreamWrapper struct { app *newrelic.Application span *newrelic.Segment // active span stream *openai.ChatCompletionStream streamResp openai.ChatCompletionResponse txn *newrelic.Transaction cw *ClientWrapper role string model string responseStr string uuid string finishReason string StreamingData map[string]interface{} isRoleAdded bool TraceID string isError bool sequence int } // Default Config func NRDefaultConfig(authToken string) *ConfigWrapper { cfg := openai.DefaultConfig(authToken) return &ConfigWrapper{ Config: &cfg, } } // Azure Config func NRDefaultAzureConfig(apiKey, baseURL string) *ConfigWrapper { cfg := openai.DefaultAzureConfig(apiKey, baseURL) return &ConfigWrapper{ Config: &cfg, } } // Call to Create Client Wrapper func NRNewClient(authToken string) *ClientWrapper { client := openai.NewClient(authToken) return &ClientWrapper{ Client: client, } } // NewClientWithConfig creates new OpenAI API client for specified config. func NRNewClientWithConfig(config *ConfigWrapper) *ClientWrapper { client := openai.NewClientWithConfig(*config.Config) return &ClientWrapper{ Client: client, } } // Adds Custom Attributes to the ClientWrapper func (cw *ClientWrapper) AddCustomAttributes(attributes map[string]interface{}) { if cw.CustomAttributes == nil { cw.CustomAttributes = make(map[string]interface{}) } for key, value := range attributes { if strings.HasPrefix(key, "llm.") { cw.CustomAttributes[key] = value } } } func AppendCustomAttributesToEvent(cw *ClientWrapper, data map[string]interface{}) map[string]interface{} { for k, v := range cw.CustomAttributes { data[k] = v } return data } // If multiple messages are sent, only the first message is used for the "content" field func GetInput(any interface{}) any { v := reflect.ValueOf(any) if v.Kind() == reflect.Array || v.Kind() == reflect.Slice { if v.Len() > 0 { // Return the first element return v.Index(0).Interface() } // Input passed in is empty return "" } return any } // Wrapper for OpenAI Streaming Recv() method // Captures the response messages as they are received in the wrapper // Once the stream is closed, the Close() method is called and sends the captured // data to New Relic func (w *ChatCompletionStreamWrapper) Recv() (openai.ChatCompletionStreamResponse, error) { response, err := w.stream.Recv() if err != nil { return response, err } if !w.isRoleAdded && (response.Choices[0].Delta.Role == "assistant" || response.Choices[0].Delta.Role == "user" || response.Choices[0].Delta.Role == "system") { w.isRoleAdded = true w.role = response.Choices[0].Delta.Role } if response.Choices[0].FinishReason != "stop" { w.responseStr += response.Choices[0].Delta.Content w.streamResp.ID = response.ID w.streamResp.Model = response.Model w.model = response.Model } finishReason, finishReasonErr := response.Choices[0].FinishReason.MarshalJSON() if finishReasonErr != nil { w.isError = true } w.finishReason = string(finishReason) return response, nil } // Close the stream and send the event to New Relic func (w *ChatCompletionStreamWrapper) Close() { w.StreamingData["response.model"] = w.model NRCreateChatCompletionMessageStream(w.app, uuid.MustParse(w.uuid), w, w.cw, w.sequence) if w.isError { w.StreamingData["error"] = true } else { w.StreamingData["response.choices.finish_reason"] = w.finishReason } w.span.End() w.app.RecordCustomEvent("LlmChatCompletionSummary", w.StreamingData) w.txn.End() w.stream.Close() } // NRCreateChatCompletionSummary captures the request data for a chat completion request // A new segment is created for the chat completion request, and the response data is timed and captured // Custom attributes are added to the event if they exist from client.AddCustomAttributes() // After closing out the custom event for the chat completion summary, the function then calls // NRCreateChatCompletionMessageInput/NRCreateChatCompletionMessage to capture the request messages func NRCreateChatCompletionSummary(txn *newrelic.Transaction, app *newrelic.Application, cw *ClientWrapper, req openai.ChatCompletionRequest) ChatCompletionResponseWrapper { // Start span txn.AddAttribute("llm", true) chatCompletionSpan := txn.StartSegment("Llm/completion/OpenAI/CreateChatCompletion") // Track Total time taken for the chat completion or embedding call to complete in milliseconds // Get App Config for setting App Name Attribute appConfig, _ := app.Config() uuid := uuid.New() spanID := txn.GetTraceMetadata().SpanID traceID := txn.GetTraceMetadata().TraceID ChatCompletionSummaryData := map[string]interface{}{} if !appConfig.AIMonitoring.Streaming.Enabled { if reportStreamingDisabled != nil { reportStreamingDisabled() } } start := time.Now() resp, err := cw.Client.CreateChatCompletion( context.Background(), req, ) duration := time.Since(start).Milliseconds() if err != nil { ChatCompletionSummaryData["error"] = true // notice error with custom attributes txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "OpenAIError", Attributes: map[string]interface{}{ "completion_id": uuid.String(), }, }) } // Request Headers ChatCompletionSummaryData["request.temperature"] = req.Temperature ChatCompletionSummaryData["request.max_tokens"] = req.MaxTokens ChatCompletionSummaryData["request.model"] = req.Model ChatCompletionSummaryData["model"] = req.Model ChatCompletionSummaryData["duration"] = duration // Response Data ChatCompletionSummaryData["response.number_of_messages"] = len(resp.Choices) + len(req.Messages) ChatCompletionSummaryData["response.model"] = resp.Model ChatCompletionSummaryData["request_id"] = resp.ID ChatCompletionSummaryData["response.organization"] = resp.Header().Get("Openai-Organization") if len(resp.Choices) > 0 { finishReason, err := resp.Choices[0].FinishReason.MarshalJSON() if err != nil { ChatCompletionSummaryData["error"] = true txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "OpenAIError", }) } else { s := string(finishReason) if len(s) > 0 && s[0] == '"' { s = s[1:] } if len(s) > 0 && s[len(s)-1] == '"' { s = s[:len(s)-1] } // strip quotes from the finish reason before setting it ChatCompletionSummaryData["response.choices.finish_reason"] = s } } // Response Headers ChatCompletionSummaryData["response.headers.llmVersion"] = resp.Header().Get("Openai-Version") ChatCompletionSummaryData["response.headers.ratelimitLimitRequests"] = resp.Header().Get("X-Ratelimit-Limit-Requests") ChatCompletionSummaryData["response.headers.ratelimitLimitTokens"] = resp.Header().Get("X-Ratelimit-Limit-Tokens") ChatCompletionSummaryData["response.headers.ratelimitResetTokens"] = resp.Header().Get("X-Ratelimit-Reset-Tokens") ChatCompletionSummaryData["response.headers.ratelimitResetRequests"] = resp.Header().Get("X-Ratelimit-Reset-Requests") ChatCompletionSummaryData["response.headers.ratelimitRemainingTokens"] = resp.Header().Get("X-Ratelimit-Remaining-Tokens") ChatCompletionSummaryData["response.headers.ratelimitRemainingRequests"] = resp.Header().Get("X-Ratelimit-Remaining-Requests") // New Relic Attributes ChatCompletionSummaryData["id"] = uuid.String() ChatCompletionSummaryData["span_id"] = spanID ChatCompletionSummaryData["trace_id"] = traceID ChatCompletionSummaryData["vendor"] = "openai" ChatCompletionSummaryData["ingest_source"] = "Go" // Record any custom attributes if they exist ChatCompletionSummaryData = AppendCustomAttributesToEvent(cw, ChatCompletionSummaryData) // Record Custom Event app.RecordCustomEvent("LlmChatCompletionSummary", ChatCompletionSummaryData) // Capture request message, returns a sequence of the messages already sent in the request. We will use that during the response message counting sequence := NRCreateChatCompletionMessageInput(txn, app, req, uuid, cw) // Capture completion messages NRCreateChatCompletionMessage(txn, app, resp, uuid, cw, sequence, req) chatCompletionSpan.End() txn.End() return ChatCompletionResponseWrapper{ ChatCompletionResponse: resp, TraceID: traceID, } } // Captures initial request messages and records a custom event in New Relic for each message // similarly to NRCreateChatCompletionMessage, but only for the request messages // Returns the sequence of the messages sent in the request // which is used to calculate the sequence in the response messages func NRCreateChatCompletionMessageInput(txn *newrelic.Transaction, app *newrelic.Application, req openai.ChatCompletionRequest, inputuuid uuid.UUID, cw *ClientWrapper) int { sequence := 0 for i, message := range req.Messages { spanID := txn.GetTraceMetadata().SpanID traceID := txn.GetTraceMetadata().TraceID appCfg, _ := app.Config() newUUID := uuid.New() newID := newUUID.String() integrationsupport.AddAgentAttribute(txn, "llm", "", true) ChatCompletionMessageData := map[string]interface{}{} // if the response doesn't have an ID, use the UUID from the summary ChatCompletionMessageData["id"] = newID // Response Data ChatCompletionMessageData["response.model"] = req.Model if appCfg.AIMonitoring.RecordContent.Enabled { ChatCompletionMessageData["content"] = message.Content } ChatCompletionMessageData["role"] = message.Role ChatCompletionMessageData["completion_id"] = inputuuid.String() // New Relic Attributes ChatCompletionMessageData["sequence"] = i ChatCompletionMessageData["vendor"] = "openai" ChatCompletionMessageData["ingest_source"] = "Go" ChatCompletionMessageData["span_id"] = spanID ChatCompletionMessageData["trace_id"] = traceID contentTokens, contentCounted := app.InvokeLLMTokenCountCallback(req.Model, message.Content) if contentCounted && app.HasLLMTokenCountCallback() { ChatCompletionMessageData["token_count"] = contentTokens } // If custom attributes are set, add them to the data ChatCompletionMessageData = AppendCustomAttributesToEvent(cw, ChatCompletionMessageData) // Record Custom Event for each message app.RecordCustomEvent("LlmChatCompletionMessage", ChatCompletionMessageData) sequence = i } return sequence } // NRCreateChatCompletionMessage captures the completion response messages and records a custom event // in New Relic for each message. The completion response messages are the responses from the model // after the request messages have been sent and logged in NRCreateChatCompletionMessageInput. // The sequence of the messages is calculated by logging each of the request messages first, then // incrementing the sequence for each response message. // The token count is calculated for each message and added to the custom event if the token count callback is set // If not, no token count is added to the custom event func NRCreateChatCompletionMessage(txn *newrelic.Transaction, app *newrelic.Application, resp openai.ChatCompletionResponse, uuid uuid.UUID, cw *ClientWrapper, sequence int, req openai.ChatCompletionRequest) { spanID := txn.GetTraceMetadata().SpanID traceID := txn.GetTraceMetadata().TraceID appCfg, _ := app.Config() integrationsupport.AddAgentAttribute(txn, "llm", "", true) sequence += 1 for i, choice := range resp.Choices { ChatCompletionMessageData := map[string]interface{}{} // if the response doesn't have an ID, use the UUID from the summary if resp.ID == "" { ChatCompletionMessageData["id"] = uuid.String() } else { ChatCompletionMessageData["id"] = resp.ID } // Request Data ChatCompletionMessageData["request.model"] = req.Model // Response Data ChatCompletionMessageData["response.model"] = resp.Model if appCfg.AIMonitoring.RecordContent.Enabled { ChatCompletionMessageData["content"] = choice.Message.Content } ChatCompletionMessageData["completion_id"] = uuid.String() ChatCompletionMessageData["role"] = choice.Message.Role // Request Headers ChatCompletionMessageData["request_id"] = resp.Header().Get("X-Request-Id") // New Relic Attributes ChatCompletionMessageData["is_response"] = true ChatCompletionMessageData["sequence"] = sequence + i ChatCompletionMessageData["vendor"] = "openai" ChatCompletionMessageData["ingest_source"] = "Go" ChatCompletionMessageData["span_id"] = spanID ChatCompletionMessageData["trace_id"] = traceID tokenCount, tokensCounted := TokenCountingHelper(app, choice.Message, resp.Model) if tokensCounted { ChatCompletionMessageData["token_count"] = tokenCount } // If custom attributes are set, add them to the data ChatCompletionMessageData = AppendCustomAttributesToEvent(cw, ChatCompletionMessageData) // Record Custom Event for each message app.RecordCustomEvent("LlmChatCompletionMessage", ChatCompletionMessageData) } } // NRCreateChatCompletionMessageStream is identical to NRCreateChatCompletionMessage, but for streaming responses. // Gets invoked only when the stream is closed func NRCreateChatCompletionMessageStream(app *newrelic.Application, uuid uuid.UUID, sw *ChatCompletionStreamWrapper, cw *ClientWrapper, sequence int) { spanID := sw.txn.GetTraceMetadata().SpanID traceID := sw.txn.GetTraceMetadata().TraceID appCfg, _ := app.Config() integrationsupport.AddAgentAttribute(sw.txn, "llm", "", true) ChatCompletionMessageData := map[string]interface{}{} // if the response doesn't have an ID, use the UUID from the summary ChatCompletionMessageData["id"] = sw.streamResp.ID // Response Data ChatCompletionMessageData["request.model"] = sw.model if appCfg.AIMonitoring.RecordContent.Enabled { ChatCompletionMessageData["content"] = sw.responseStr } ChatCompletionMessageData["role"] = sw.role ChatCompletionMessageData["is_response"] = true // New Relic Attributes ChatCompletionMessageData["sequence"] = sequence + 1 ChatCompletionMessageData["vendor"] = "openai" ChatCompletionMessageData["ingest_source"] = "Go" ChatCompletionMessageData["completion_id"] = uuid.String() ChatCompletionMessageData["span_id"] = spanID ChatCompletionMessageData["trace_id"] = traceID tmpMessage := openai.ChatCompletionMessage{ Content: sw.responseStr, Role: sw.role, // Name is not provided in the stream response, so we don't include it in token counting Name: "", } tokenCount, tokensCounted := TokenCountingHelper(app, tmpMessage, sw.model) if tokensCounted { ChatCompletionMessageData["token_count"] = tokenCount } // If custom attributes are set, add them to the data ChatCompletionMessageData = AppendCustomAttributesToEvent(cw, ChatCompletionMessageData) // Record Custom Event for each message app.RecordCustomEvent("LlmChatCompletionMessage", ChatCompletionMessageData) } // Calculates tokens using the LLmTokenCountCallback // In order to calculate total tokens of a message, we need to factor in the Content, Role, and Name (if it exists) func TokenCountingHelper(app *newrelic.Application, message openai.ChatCompletionMessage, model string) (numTokens int, tokensCounted bool) { contentTokens, contentCounted := app.InvokeLLMTokenCountCallback(model, message.Content) roleTokens, roleCounted := app.InvokeLLMTokenCountCallback(model, message.Role) var messageTokens int if message.Name != "" { messageTokens, _ = app.InvokeLLMTokenCountCallback(model, message.Name) } numTokens += contentTokens + roleTokens + messageTokens return numTokens, (contentCounted && roleCounted) } // Similar to NRCreateChatCompletionSummary, but for streaming responses // Returns a custom wrapper with a stream that can be used to receive messages // Example Usage: /* ctx := context.Background() stream, err := nropenai.NRCreateChatCompletionStream(client, ctx, req, app) if err != nil { panic(err) } for { var response openai.ChatCompletionStreamResponse response, err = stream.Recv() if errors.Is(err, io.EOF) { fmt.Println("\nStream finished") break } if err != nil { fmt.Printf("\nStream error: %v\n", err) return } fmt.Printf(response.Choices[0].Delta.Content) } stream.Close() */ // It is important to call stream.Close() after the stream has been used, as it will close the stream and send the event to New Relic. // Additionally, custom attributes can be added to the client using client.AddCustomAttributes(map[string]interface{}) just like in NRCreateChatCompletionSummary func NRCreateChatCompletionStream(cw *ClientWrapper, ctx context.Context, req openai.ChatCompletionRequest, app *newrelic.Application) (*ChatCompletionStreamWrapper, error) { txn := app.StartTransaction("OpenAIChatCompletionStream") config, _ := app.Config() if !config.AIMonitoring.Streaming.Enabled { if reportStreamingDisabled != nil { reportStreamingDisabled() } } // If AI Monitoring OR AIMonitoring.Streaming is disabled, do not start a transaction but still perform the request if !config.AIMonitoring.Enabled || !config.AIMonitoring.Streaming.Enabled { stream, err := cw.Client.CreateChatCompletionStream(ctx, req) if err != nil { return &ChatCompletionStreamWrapper{stream: stream}, err } return &ChatCompletionStreamWrapper{stream: stream}, errAIMonitoringDisabled } streamSpan := txn.StartSegment("Llm/completion/OpenAI/CreateChatCompletion") spanID := txn.GetTraceMetadata().SpanID traceID := txn.GetTraceMetadata().TraceID StreamingData := map[string]interface{}{} uuid := uuid.New() integrationsupport.AddAgentAttribute(txn, "llm", "", true) start := time.Now() stream, err := cw.Client.CreateChatCompletionStream(ctx, req) duration := time.Since(start).Milliseconds() if err != nil { StreamingData["error"] = true txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "OpenAIError", }) txn.End() return nil, err } // Request Data StreamingData["request.model"] = string(req.Model) StreamingData["request.temperature"] = req.Temperature StreamingData["request.max_tokens"] = req.MaxTokens StreamingData["model"] = req.Model StreamingData["duration"] = duration // New Relic Attributes StreamingData["id"] = uuid.String() StreamingData["span_id"] = spanID StreamingData["trace_id"] = traceID StreamingData["vendor"] = "openai" StreamingData["ingest_source"] = "Go" sequence := NRCreateChatCompletionMessageInput(txn, app, req, uuid, cw) return &ChatCompletionStreamWrapper{ app: app, stream: stream, txn: txn, span: streamSpan, uuid: uuid.String(), cw: cw, StreamingData: StreamingData, TraceID: traceID, sequence: sequence}, nil } // NRCreateChatCompletion is a wrapper for the OpenAI CreateChatCompletion method. // If AI Monitoring is disabled, the wrapped function will still call the OpenAI CreateChatCompletion method // and return the response with no New Relic instrumentation // Calls NRCreateChatCompletionSummary to capture the request data and response data // Returns a ChatCompletionResponseWrapper with the response and the TraceID of the transaction // The trace ID is used to link the chat response with its feedback, with a call to SendFeedback() // Otherwise, the response is the same as the OpenAI CreateChatCompletion method. It can be accessed // by calling resp.ChatCompletionResponse func NRCreateChatCompletion(cw *ClientWrapper, req openai.ChatCompletionRequest, app *newrelic.Application) (ChatCompletionResponseWrapper, error) { config, _ := app.Config() resp := ChatCompletionResponseWrapper{} // If AI Monitoring is disabled, do not start a transaction but still perform the request if !config.AIMonitoring.Enabled { chatresp, err := cw.Client.CreateChatCompletion(context.Background(), req) resp.ChatCompletionResponse = chatresp if err != nil { return resp, err } return resp, errAIMonitoringDisabled } // Start NR Transaction txn := app.StartTransaction("OpenAIChatCompletion") resp = NRCreateChatCompletionSummary(txn, app, cw, req) return resp, nil } // NRCreateEmbedding is a wrapper for the OpenAI CreateEmbedding method. // If AI Monitoring is disabled, the wrapped function will still call the OpenAI CreateEmbedding method and return the response with no New Relic instrumentation func NRCreateEmbedding(cw *ClientWrapper, req openai.EmbeddingRequest, app *newrelic.Application) (openai.EmbeddingResponse, error) { config, _ := app.Config() resp := openai.EmbeddingResponse{} // If AI Monitoring is disabled, do not start a transaction but still perform the request if !config.AIMonitoring.Enabled { resp, err := cw.Client.CreateEmbeddings(context.Background(), req) if err != nil { return resp, err } return resp, errAIMonitoringDisabled } // Start NR Transaction txn := app.StartTransaction("OpenAIEmbedding") embeddingSpan := txn.StartSegment("Llm/embedding/OpenAI/CreateEmbedding") spanID := txn.GetTraceMetadata().SpanID traceID := txn.GetTraceMetadata().TraceID EmbeddingsData := map[string]interface{}{} uuid := uuid.New() integrationsupport.AddAgentAttribute(txn, "llm", "", true) start := time.Now() resp, err := cw.Client.CreateEmbeddings(context.Background(), req) duration := time.Since(start).Milliseconds() embeddingSpan.End() if err != nil { EmbeddingsData["error"] = true txn.NoticeError(newrelic.Error{ Message: err.Error(), Class: "OpenAIError", Attributes: map[string]interface{}{ "embedding_id": uuid.String(), }, }) } // Request Data if config.AIMonitoring.RecordContent.Enabled { EmbeddingsData["input"] = GetInput(req.Input) } EmbeddingsData["request_id"] = resp.Header().Get("X-Request-Id") EmbeddingsData["request.model"] = string(req.Model) EmbeddingsData["duration"] = duration // Response Data EmbeddingsData["response.model"] = string(resp.Model) // cast input as string input := GetInput(req.Input).(string) tokenCount, tokensCounted := app.InvokeLLMTokenCountCallback(string(resp.Model), input) if tokensCounted && app.HasLLMTokenCountCallback() { EmbeddingsData["token_count"] = tokenCount } // Response Headers EmbeddingsData["response.organization"] = resp.Header().Get("Openai-Organization") EmbeddingsData["response.headers.llmVersion"] = resp.Header().Get("Openai-Version") EmbeddingsData["response.headers.ratelimitLimitRequests"] = resp.Header().Get("X-Ratelimit-Limit-Requests") EmbeddingsData["response.headers.ratelimitLimitTokens"] = resp.Header().Get("X-Ratelimit-Limit-Tokens") EmbeddingsData["response.headers.ratelimitResetTokens"] = resp.Header().Get("X-Ratelimit-Reset-Tokens") EmbeddingsData["response.headers.ratelimitResetRequests"] = resp.Header().Get("X-Ratelimit-Reset-Requests") EmbeddingsData["response.headers.ratelimitRemainingTokens"] = resp.Header().Get("X-Ratelimit-Remaining-Tokens") EmbeddingsData["response.headers.ratelimitRemainingRequests"] = resp.Header().Get("X-Ratelimit-Remaining-Requests") EmbeddingsData = AppendCustomAttributesToEvent(cw, EmbeddingsData) // New Relic Attributes EmbeddingsData["id"] = uuid.String() EmbeddingsData["vendor"] = "openai" EmbeddingsData["ingest_source"] = "Go" EmbeddingsData["span_id"] = spanID EmbeddingsData["trace_id"] = traceID app.RecordCustomEvent("LlmEmbedding", EmbeddingsData) txn.End() return resp, nil } go-agent-3.42.0/v3/integrations/nropenai/nropenai_test.go000066400000000000000000000542161510742411500233630ustar00rootroot00000000000000package nropenai import ( "context" "errors" "net/http" "testing" "github.com/google/uuid" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/sashabaranov/go-openai" ) type MockOpenAIClient struct { MockCreateChatCompletionResp openai.ChatCompletionResponse MockCreateEmbeddingsResp openai.EmbeddingResponse MockCreateChatCompletionStream *openai.ChatCompletionStream MockCreateChatCompletionErr error } // Mock CreateChatCompletion function that returns a mock response func (m *MockOpenAIClient) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) { MockResponse := openai.ChatCompletionResponse{ ID: "chatcmpl-123", Object: "chat.completion", Created: 1677652288, Model: openai.GPT3Dot5Turbo, SystemFingerprint: "fp_44709d6fcb", Usage: openai.Usage{ PromptTokens: 9, CompletionTokens: 12, TotalTokens: 21, }, Choices: []openai.ChatCompletionChoice{ { Index: 0, Message: openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, Content: "\n\nHello there, how may I assist you today?", }, }, }, } hdrs := http.Header{} hdrs.Add("X-Request-Id", "chatcmpl-123") hdrs.Add("ratelimit-limit-tokens", "100") hdrs.Add("Openai-Version", "2020-10-01") hdrs.Add("X-Ratelimit-Limit-Requests", "10000") hdrs.Add("X-Ratelimit-Limit-Tokens", "100") hdrs.Add("X-Ratelimit-Reset-Tokens", "100") hdrs.Add("X-Ratelimit-Reset-Requests", "10000") hdrs.Add("X-Ratelimit-Remaining-Tokens", "100") hdrs.Add("X-Ratelimit-Remaining-Requests", "10000") hdrs.Add("Openai-Organization", "user-123") if req.Messages[0].Content == "testError" { mockRespErr := openai.ChatCompletionResponse{} hdrs.Add("Status", "404") hdrs.Add("Error-Code", "404") mockRespErr.SetHeader(hdrs) return mockRespErr, errors.New("test error") } MockResponse.SetHeader(hdrs) return MockResponse, m.MockCreateChatCompletionErr } func (m *MockOpenAIClient) CreateEmbeddings(ctx context.Context, conv openai.EmbeddingRequestConverter) (res openai.EmbeddingResponse, err error) { MockResponse := openai.EmbeddingResponse{ Model: openai.AdaEmbeddingV2, Usage: openai.Usage{ PromptTokens: 9, CompletionTokens: 12, TotalTokens: 21, }, Data: []openai.Embedding{ { Embedding: []float32{0.1, 0.2, 0.3}, }, }, } hdrs := http.Header{} hdrs.Add("X-Request-Id", "chatcmpl-123") hdrs.Add("ratelimit-limit-tokens", "100") hdrs.Add("Openai-Version", "2020-10-01") hdrs.Add("X-Ratelimit-Limit-Requests", "10000") hdrs.Add("X-Ratelimit-Limit-Tokens", "100") hdrs.Add("X-Ratelimit-Reset-Tokens", "100") hdrs.Add("X-Ratelimit-Reset-Requests", "10000") hdrs.Add("X-Ratelimit-Remaining-Tokens", "100") hdrs.Add("X-Ratelimit-Remaining-Requests", "10000") hdrs.Add("Openai-Organization", "user-123") cv := conv.Convert() if cv.Input == "testError" { mockRespErr := openai.EmbeddingResponse{} hdrs.Add("Status", "404") hdrs.Add("Error-Code", "404") mockRespErr.SetHeader(hdrs) return mockRespErr, errors.New("test error") } MockResponse.SetHeader(hdrs) return MockResponse, m.MockCreateChatCompletionErr } func (m *MockOpenAIClient) CreateChatCompletionStream(ctx context.Context, request openai.ChatCompletionRequest) (stream *openai.ChatCompletionStream, err error) { if request.Messages[0].Content == "testError" { return m.MockCreateChatCompletionStream, errors.New("test error") } return m.MockCreateChatCompletionStream, m.MockCreateChatCompletionErr } func TestDefaultConfig(t *testing.T) { dummyAPIKey := "sk-12345678900abcdefghijklmnop" cfg := NRDefaultConfig(dummyAPIKey) // Default Values if cfg.Config.OrgID != "" { t.Errorf("OrgID is incorrect: expected: %s actual: %s", "", cfg.Config.OrgID) } // Default Value set by openai package if cfg.Config.APIType != openai.APITypeOpenAI { t.Errorf("API Type is incorrect: expected: %s actual: %s", openai.APITypeOpenAI, cfg.Config.APIType) } } func TestDefaultConfigAzure(t *testing.T) { dummyAPIKey := "sk-12345678900abcdefghijklmnop" baseURL := "https://azure-base-url.com" cfg := NRDefaultAzureConfig(dummyAPIKey, baseURL) // Default Values if cfg.Config.BaseURL != baseURL { t.Errorf("baseURL is incorrect: expected: %s actual: %s", baseURL, cfg.Config.BaseURL) } // Default Value set by openai package if cfg.Config.APIType != openai.APITypeAzure { t.Errorf("API Type is incorrect: expected: %s actual: %s", openai.APITypeAzure, cfg.Config.APIType) } } func TestAddCustomAttributes(t *testing.T) { client := NRNewClient("sk-12345678900abcdefghijklmnop") client.AddCustomAttributes(map[string]interface{}{ "llm.foo": "bar", }) if client.CustomAttributes["llm.foo"] != "bar" { t.Errorf("Custom attribute is incorrect: expected: %s actual: %s", "bar", client.CustomAttributes["llm.foo"]) } } func TestAddCustomAttributesIncorrectPrefix(t *testing.T) { client := NRNewClient("sk-12345678900abcdefghijklmnop") client.AddCustomAttributes(map[string]interface{}{ "msdwmdoawd.foo": "bar", }) if len(client.CustomAttributes) != 0 { t.Errorf("Custom attribute is incorrect: expected: %d actual: %d", 0, len(client.CustomAttributes)) } } func TestNRCreateChatCompletion(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 150, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "What is 8*5", }, }, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) resp, err := NRCreateChatCompletion(cw, req, app.Application) if err != nil { t.Error(err) } if resp.ChatCompletionResponse.Choices[0].Message.Content != "\n\nHello there, how may I assist you today?" { t.Errorf("Chat completion response is incorrect: expected: %s actual: %s", "\n\nHello there, how may I assist you today?", resp.ChatCompletionResponse.Choices[0].Message.Content) } app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionSummary", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "ingest_source": "Go", "vendor": "openai", "model": "gpt-3.5-turbo", "id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "duration": 0, "response.choices.finish_reason": internal.MatchAnything, "request.temperature": 0, "request_id": "chatcmpl-123", "request.model": "gpt-3.5-turbo", "request.max_tokens": 150, "response.number_of_messages": 2, "response.headers.llmVersion": "2020-10-01", "response.organization": "user-123", "response.model": "gpt-3.5-turbo", "response.headers.ratelimitRemainingTokens": "100", "response.headers.ratelimitRemainingRequests": "10000", "response.headers.ratelimitResetTokens": "100", "response.headers.ratelimitResetRequests": "10000", "response.headers.ratelimitLimitTokens": "100", "response.headers.ratelimitLimitRequests": "10000", }, }, { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "completion_id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "id": internal.MatchAnything, "sequence": 0, "role": "user", "content": "What is 8*5", "vendor": "openai", "ingest_source": "Go", "response.model": "gpt-3.5-turbo", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "completion_id": internal.MatchAnything, "id": "chatcmpl-123", "sequence": 1, "role": "assistant", "content": "\n\nHello there, how may I assist you today?", "request_id": "chatcmpl-123", "vendor": "openai", "ingest_source": "Go", "is_response": true, "response.model": "gpt-3.5-turbo", "request.model": "gpt-3.5-turbo", }, AgentAttributes: map[string]interface{}{}, }, }) } func TestNRCreateChatCompletionAIMonitoringNotEnabled(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 150, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "What is 8*5", }, }, } app := integrationsupport.NewTestApp(nil) resp, err := NRCreateChatCompletion(cw, req, app.Application) if err != errAIMonitoringDisabled { t.Error(err) } // If AI Monitoring is disabled, no events should be sent, but a response from OpenAI should still be returned if resp.ChatCompletionResponse.Choices[0].Message.Content != "\n\nHello there, how may I assist you today?" { t.Errorf("Chat completion response is incorrect: expected: %s actual: %s", "\n\nHello there, how may I assist you today?", resp.ChatCompletionResponse.Choices[0].Message.Content) } app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestNRCreateChatCompletionError(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 150, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "testError", }, }, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) _, err := NRCreateChatCompletion(cw, req, app.Application) if err != nil { t.Error(err) } app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionSummary", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "error": true, "ingest_source": "Go", "vendor": "openai", "model": "gpt-3.5-turbo", "id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "duration": 0, "request.temperature": 0, "request_id": "", "request.model": "gpt-3.5-turbo", "request.max_tokens": 150, "response.number_of_messages": 1, "response.headers.llmVersion": "2020-10-01", "response.organization": "user-123", "response.model": "", "response.headers.ratelimitRemainingTokens": "100", "response.headers.ratelimitRemainingRequests": "10000", "response.headers.ratelimitResetTokens": "100", "response.headers.ratelimitResetRequests": "10000", "response.headers.ratelimitLimitTokens": "100", "response.headers.ratelimitLimitRequests": "10000", }, }, { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "completion_id": internal.MatchAnything, "ingest_source": "Go", "vendor": "openai", "id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "content": "testError", "role": "user", "response.model": "gpt-3.5-turbo", "sequence": 0, }, }, }) app.ExpectErrorEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "TransactionError", "transactionName": "OtherTransaction/Go/OpenAIChatCompletion", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "error.class": "OpenAIError", "error.message": "test error", }, UserAttributes: map[string]interface{}{ "completion_id": internal.MatchAnything, "llm": true, }, }, }) } func TestNRCreateEmbedding(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } embeddingReq := openai.EmbeddingRequest{ Input: []string{ "The food was delicious and the waiter", "Other examples of embedding request", }, Model: openai.AdaEmbeddingV2, EncodingFormat: openai.EmbeddingEncodingFormatFloat, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) _, err := NRCreateEmbedding(cw, embeddingReq, app.Application) if err != nil { t.Error(err) } app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmEmbedding", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "ingest_source": "Go", "vendor": "openai", "id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "duration": 0, "request_id": "chatcmpl-123", "request.model": "text-embedding-ada-002", "response.headers.llmVersion": "2020-10-01", "response.organization": "user-123", "response.model": "text-embedding-ada-002", "input": "The food was delicious and the waiter", "response.headers.ratelimitRemainingTokens": "100", "response.headers.ratelimitRemainingRequests": "10000", "response.headers.ratelimitResetTokens": "100", "response.headers.ratelimitResetRequests": "10000", "response.headers.ratelimitLimitTokens": "100", "response.headers.ratelimitLimitRequests": "10000", }, }, }) } func TestNRCreateEmbeddingAIMonitoringNotEnabled(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } embeddingReq := openai.EmbeddingRequest{ Input: []string{ "The food was delicious and the waiter", "Other examples of embedding request", }, Model: openai.AdaEmbeddingV2, EncodingFormat: openai.EmbeddingEncodingFormatFloat, } app := integrationsupport.NewTestApp(nil) resp, err := NRCreateEmbedding(cw, embeddingReq, app.Application) if err != errAIMonitoringDisabled { t.Error(err) } // If AI Monitoring is disabled, no events should be sent, but a response from OpenAI should still be returned app.ExpectCustomEvents(t, []internal.WantEvent{}) if resp.Data[0].Embedding[0] != 0.1 { t.Errorf("Embedding response is incorrect: expected: %f actual: %f", 0.1, resp.Data[0].Embedding[0]) } } func TestNRCreateEmbeddingError(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } embeddingReq := openai.EmbeddingRequest{ Input: "testError", Model: openai.AdaEmbeddingV2, EncodingFormat: openai.EmbeddingEncodingFormatFloat, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) _, err := NRCreateEmbedding(cw, embeddingReq, app.Application) if err != nil { t.Error(err) } app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmEmbedding", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "ingest_source": "Go", "vendor": "openai", "id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "duration": 0, "request_id": "chatcmpl-123", "request.model": "text-embedding-ada-002", "response.headers.llmVersion": "2020-10-01", "response.organization": "user-123", "error": true, "response.model": "", "input": "testError", "response.headers.ratelimitRemainingTokens": "100", "response.headers.ratelimitRemainingRequests": "10000", "response.headers.ratelimitResetTokens": "100", "response.headers.ratelimitResetRequests": "10000", "response.headers.ratelimitLimitTokens": "100", "response.headers.ratelimitLimitRequests": "10000", }, }, }) app.ExpectErrorEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "TransactionError", "transactionName": "OtherTransaction/Go/OpenAIEmbedding", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "error.class": "OpenAIError", "error.message": "test error", }, UserAttributes: map[string]interface{}{ "embedding_id": internal.MatchAnything, }, }}) } func TestNRCreateChatCompletionMessageStream(t *testing.T) { mockStreamWrapper := ChatCompletionStreamWrapper{} mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) txn := app.StartTransaction("NRCreateChatCompletionMessageStream") uuid := uuid.New() mockStreamWrapper.txn = txn mockStreamWrapper.finishReason = "stop" mockStreamWrapper.uuid = uuid.String() mockStreamWrapper.isError = false mockStreamWrapper.responseStr = "Hello there, how may I assist you today?" mockStreamWrapper.role = openai.ChatMessageRoleAssistant mockStreamWrapper.model = "gpt-3.5-turbo" mockStreamWrapper.sequence = 1 NRCreateChatCompletionMessageStream(app.Application, uuid, &mockStreamWrapper, cw, 1) txn.End() app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "completion_id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "id": internal.MatchAnything, "sequence": 2, "role": "assistant", "content": "Hello there, how may I assist you today?", "vendor": "openai", "ingest_source": "Go", "request.model": "gpt-3.5-turbo", "is_response": true, }, AgentAttributes: map[string]interface{}{}, }, }) } func TestNRCreateStream(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 1500, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "Say this is a test", }, }, Stream: true, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) _, err := NRCreateChatCompletionStream(cw, context.Background(), req, app.Application) if err != nil { t.Error(err) } app.ExpectCustomEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "LlmChatCompletionMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "completion_id": internal.MatchAnything, "trace_id": internal.MatchAnything, "span_id": internal.MatchAnything, "id": internal.MatchAnything, "sequence": 0, "role": "user", "content": "Say this is a test", "vendor": "openai", "ingest_source": "Go", "response.model": "gpt-3.5-turbo", }, AgentAttributes: map[string]interface{}{}, }, }) } func TestNRCreateStreamAIMonitoringNotEnabled(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 1500, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "Say this is a test", }, }, Stream: true, } app := integrationsupport.NewTestApp(nil) _, err := NRCreateChatCompletionStream(cw, context.Background(), req, app.Application) if err != errAIMonitoringDisabled { t.Error(err) } app.ExpectCustomEvents(t, []internal.WantEvent{}) app.ExpectTxnEvents(t, []internal.WantEvent{}) } func TestNRCreateStreamError(t *testing.T) { mockClient := &MockOpenAIClient{} cw := &ClientWrapper{ Client: mockClient, } req := openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Temperature: 0, MaxTokens: 1500, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: "testError", }, }, Stream: true, } app := integrationsupport.NewTestApp(nil, newrelic.ConfigAIMonitoringEnabled(true)) _, err := NRCreateChatCompletionStream(cw, context.Background(), req, app.Application) if err.Error() != "test error" { t.Error(err) } app.ExpectErrorEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "type": "TransactionError", "transactionName": "OtherTransaction/Go/OpenAIChatCompletionStream", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "error.class": "OpenAIError", "error.message": "test error", }, }}) } go-agent-3.42.0/v3/integrations/nrpgx/000077500000000000000000000000001510742411500174755ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx/LICENSE.txt000066400000000000000000000264501510742411500213270ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx/README.md000066400000000000000000000006521510742411500207570ustar00rootroot00000000000000# v3/integrations/nrpgx [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx) Package `nrpgx` instruments https://github.com/jackc/pgx/v4. ```go import "github.com/newrelic/go-agent/v3/integrations/nrpgx" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx). go-agent-3.42.0/v3/integrations/nrpgx/example/000077500000000000000000000000001510742411500211305ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx/example/sql_compat/000077500000000000000000000000001510742411500232725ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx/example/sql_compat/main.go000066400000000000000000000034661510742411500245560ustar00rootroot00000000000000// Copyright 2020, 2021 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // Example of using nrpgx to instrument a Postgres database application // using the jackc/pgx driver with database/sql. // // To run this example, be sure the environment variable NEW_RELIC_LICENSE_KEY // is set to your license key. Postgres must be running on the default port // 5432 on localhost, and have a password "docker". An easy (albeit insecure) // way to test this is to issue the following command to run a postgres database // in a docker container: // docker run --rm -e POSTGRES_PASSWORD=docker -p 5432:5432 postgres // // Run that in the background or in a separate window, and then run this program // to access that database. // package main import ( "context" "database/sql" "fmt" "os" "time" _ "github.com/newrelic/go-agent/v3/integrations/nrpgx" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // docker run --rm -e POSTGRES_PASSWORD=docker -p 5432:5432 postgres db, err := sql.Open("nrpgx", "host=localhost port=5432 user=postgres dbname=postgres password=docker sslmode=disable") if err != nil { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("PostgreSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // // N.B.: We do not recommend using app.WaitForConnection in production code. // app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("postgresQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") var count int row.Scan(&count) txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of entries in pg_catalog.pg_tables", count) } go-agent-3.42.0/v3/integrations/nrpgx/example/sqlx/000077500000000000000000000000001510742411500221175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx/example/sqlx/LICENSE.txt000066400000000000000000000264501510742411500237510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx/example/sqlx/go.mod000066400000000000000000000007021510742411500232240ustar00rootroot00000000000000// This sqlx example is a separate module to avoid adding sqlx dependency to the // nrpgx go.mod file. module github.com/newrelic/go-agent/v3/integrations/nrpgx/example/sqlx go 1.24 require ( github.com/jmoiron/sqlx v1.2.0 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrpgx v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrpgx => ../../ replace github.com/newrelic/go-agent/v3 => ../../../.. go-agent-3.42.0/v3/integrations/nrpgx/example/sqlx/main.go000066400000000000000000000112521510742411500233730ustar00rootroot00000000000000// Copyright 2020, 2021 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // An application that illustrates how to instrument jmoiron/sqlx with DatastoreSegments // // To run this example, be sure the environment varible NEW_RELIC_LICENSE_KEY // is set to your license key. Postgres must be running on the default port // 5432 and have a user "foo" and a database "bar". One quick (albeit insecure) // way of doing this is to run a small local Postgres instance in Docker: // // docker run --rm -e POSTGRES_USER=foo -e POSTGRES_DB=bar \ // -e POSTGRES_PASSWORD=password -e POSTGRES_HOST_AUTH_METHOD=trust \ // -p 5432:5432 postgres & // // Adding instrumentation for the SQLx package is easy. It means you can // make database calls without having to manually create DatastoreSegments. // Setup can be done in two steps: // // # Set up your driver // // If you are using one of our currently supported database drivers (see // https://docs.newrelic.com/docs/agents/go-agent/get-started/go-agent-compatibility-requirements#frameworks), // follow the instructions on installing the driver. // // As an example, for the `pgx` driver, you will use the newrelic // integration's driver in place of the postgres driver. If your code is using // sqlx.Open with `pgx` like this: // // import ( // "github.com/jmoiron/sqlx" // _ "github.com/jackc/pgx" // ) // // func main() { // db, err := sqlx.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // Then change the side-effect import to the integration package, and open // "nrpgx" instead: // // import ( // "github.com/jmoiron/sqlx" // _ "github.com/newrelic/go-agent/v3/integrations/nrpgx" // ) // // func main() { // db, err := sqlx.Open("nrpgx", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // If you are not using one of the supported database drivers, use the // `InstrumentSQLDriver` // (https://godoc.org/github.com/newrelic/go-agent#InstrumentSQLDriver) API. // See // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // for a full example. // // # Add context to your database calls // // Next, you must provide a context containing a newrelic.Transaction to all // methods on sqlx.DB, sqlx.NamedStmt, sqlx.Stmt, and sqlx.Tx that make a // database call. For example, instead of the following: // // err := db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // err := db.GetContext(ctx, &jason, "SELECT * FROM person WHERE first_name=$1", "Jason") package main import ( "context" "log" "os" "time" "github.com/jmoiron/sqlx" _ "github.com/newrelic/go-agent/v3/integrations/nrpgx" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) var schema = ` CREATE TABLE person ( first_name text, last_name text, email text )` // Person is a person in the database type Person struct { FirstName string `db:"first_name"` LastName string `db:"last_name"` Email string } func createApp() *newrelic.Application { app, err := newrelic.NewApplication( newrelic.ConfigAppName("SQLx"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { log.Fatalln(err) } // // DO NOT USE WaitForConnection in production code! // if err := app.WaitForConnection(5 * time.Second); err != nil { log.Fatalln(err) } return app } func main() { // Create application app := createApp() defer app.Shutdown(10 * time.Second) // Start a transaction txn := app.StartTransaction("main") defer txn.End() // Add transaction to context ctx := newrelic.NewContext(context.Background(), txn) // Connect to database using the "nrpgx" driver db, err := sqlx.Connect("nrpgx", "host=localhost user=foo dbname=bar sslmode=disable") if err != nil { log.Fatalln(err) } // Create database table if it does not exist already // When the context is passed, DatastoreSegments will be created db.ExecContext(ctx, schema) // Add people to the database // When the context is passed, DatastoreSegments will be created tx := db.MustBegin() tx.MustExecContext(ctx, "INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "Jason", "Moiron", "jmoiron@jmoiron.net") tx.MustExecContext(ctx, "INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "John", "Doe", "johndoeDNE@gmail.net") tx.Commit() // Read from the database // When the context is passed, DatastoreSegments will be created people := []Person{} db.SelectContext(ctx, &people, "SELECT * FROM person ORDER BY first_name ASC") jason := Person{} db.GetContext(ctx, &jason, "SELECT * FROM person WHERE first_name=$1", "Jason") } go-agent-3.42.0/v3/integrations/nrpgx/go.mod000066400000000000000000000003671510742411500206110ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrpgx go 1.24 require ( github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx/v4 v4.18.2 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrpgx/nrpgx.go000066400000000000000000000244321510742411500211670ustar00rootroot00000000000000// Copyright 2021 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 // Package nrpgx instruments https://github.com/jackc/pgx/v4. // // Use this package to instrument your PostgreSQL calls using the pgx // library. // // USING WITH PGX AS A DATABASE/SQL DRIVER // // The pgx library may be used as a database/sql driver rather than making // direct calls into pgx itself. In this scenario, just use the nrpgx integration // in place of the pgx driver. In other words, // if your code without New Relic's agent looks like this: // // import ( // "database/sql" // _ "github.com/jackc/pgx/v4/stdlib" // ) // // func main() { // db, err := sql.Open("pgx", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // Then change the side-effect import to this package, and open "nrpgx" instead: // // import ( // "database/sql" // // _ "github.com/newrelic/go-agent/v3/integrations/nrpgx" // ) // // func main() { // db, err := sql.Open("nrpgx", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // Next, provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, and sql.Tx. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) FROM pg_catalog.pg_tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") // // A working example is shown here: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrpgx/example/sql_compat/main.go // // USING WITH DIRECT PGX CALLS WITHOUT DATABASE/SQL // // This mode of operation is not supported by the nrpgx integration at this time. package nrpgx import ( "database/sql" "os" "path" "regexp" "strconv" "strings" "github.com/jackc/pgx" "github.com/jackc/pgx/v4/stdlib" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastorePostgres, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN(os.Getenv), } ) func init() { sql.Register("nrpgx", newrelic.InstrumentSQLDriver(&stdlib.Driver{}, baseBuilder)) internal.TrackUsage("integration", "driver", "nrpgx") } /* * The general format for these is: * postgres[ql]://[user[:pass]@][host][:port][,...][/db][?params] * * pgx recognizes: * sql.Open("pgx", "hostname=x host_port=x username=x password=x databasename=x sslmode=x") * or * sql.Open("pgx", URI) * or * driverConfig := stdlib.DriverConfig{...} * sql.RegisterDriverConfig(&driverConfig) * sql.Open("pgx", driverConfig.ConnectionString(URI)) * * params may include * host=name (see below) * hostaddr=IPv4/IPv6 address * port=number * dbname=name * user=name * password=string * passfile=path * connect_timeout=sec * client_encoding=scheme|"auto" * options=command-line opts * application_name=name * fallback_appliction_name=name * keepalives=0|1 * keepalives_idle=sec * keepalives_interval=sec * keepalives_count=n * tcp_user_timeout=ms * tty=name * replication=mode * gssencmode=mode * sslmode=disable|allow|prefer|require|verify-ca|verify-full * requiressl=bool * sslcompression=bool * sslcert=file * sslkey=file * sslrootcert=file * sslcrl=file * requirepeer=user * krbsrvname=name * gsslib=name * service=name * target_session_attrs=value * ssl=true is equivalent to sslmode=require * * may include %-encoded values * host may be a DNS name, IP4 address, or IP6 address in square brackets, or may be * a pathname for a UNIX domain socket (but since slash is reserved in URI syntax, * this may need to be put in the params list) * (Note that the IP6 address contains colons) * multiple hosts may be given as a comma-separated list * * pgx provides * ParseConnectionString(string) -> ConnConfig, error (URI or DSN) * ParseDSN(string) -> ConnConfig, error (DSN only) * ParseURI(string) -> ConnConfig, error (URI only) * * which we'll use to parse out all of the above and avoid second-guessing * anything or duplicating effort. */ // parsePort takes a string representation which purports to be a TCP port number // or a comma-separated list of port numbers, and returns the port number as a // uint16 value and as a string. If a list was given, the returned values are for // the first item in the list, discarding the others. // // If the string can't be understood as an unsigned integer value, then 0 is // returned as the uint16 value. func parsePort(p string) (uint16, string) { if p == "" { return 0, "" } np, err := strconv.ParseUint(p, 10, 16) if err != nil { // Maybe multiple values were specified? // (The code could be more concise if we did more processing up front, // but this avoids needless work if a single number was given, so should // be more runtime efficient.) if comma := strings.IndexRune(p, ','); comma >= 0 { p = p[0:comma] np, err = strconv.ParseUint(p, 10, 16) if err != nil { // even the first one isn't a real number np = 0 } } else { // Nope, it's just something we can't grok np = 0 } } return uint16(np), p } // ip6HostPort is a regular expression to parse IPv6 hostnames (which must be in // square brackets in the connection strings we're working with). The hostname may // optionally be followed by a colon and a TCP port number. // // Any text after the hostname (and port, if any), is ignored. var ip6HostPort = regexp.MustCompile(`^\[([0-9a-fA-F:]*)\](:([0-9]+))?`) // fullIp6ConnectPattern and fullConnectPattern are regular expressions which // match a postgres URI connection string using IPv6 addresses, or other forms, // respectively. var fullIp6ConnectPattern = regexp.MustCompile( // user password host port dbname params // __1__ __2__ _________3________ __4__ __5__ __6_ `^postgres(?:ql)?://(?:(.*?)(?::(.*?))?@)?(\[[0-9a-fA-F:]+\])(?::(.*?))?(?:,.*?)*(?:/(.*?))?(?:\?(.*))?$`) var fullConnectPattern = regexp.MustCompile( // user password host port dbname params // __1__ __2__ ________3________ __4__ __5__ __6_ `^postgres(?:ql)?://(?:(.*?)(?::(.*?))?@)?([a-zA-Z0-9_.-]+)(?::(.*?))?(?:,.*?)*(?:/(.*?))?(?:\?(.*))?$`) // fullParamPattern is a regular expression to match the key=value pairs of a // parameterized DSN string. var fullParamPattern = regexp.MustCompile( `(\w+)\s*=\s*('[^=]*'|[^'\s]+)`) // parseDSN returns a function which will set datastore segment attributes to show // the database, host, and port as extracted from a supplied DSN string. func parseDSN(getenv func(string) string) func(*newrelic.DatastoreSegment, string) { return func(s *newrelic.DatastoreSegment, dsn string) { cc, err := pgx.ParseConnectionString(dsn) if err != nil { // the connection string is invalid // Sometimes we've found that pgx.ParseConnectionString doesn't recognize // all patterns so if that call failed, we'll do a little pattern matching // of our own and see if we can figure it out. cc = pgx.ConnConfig{} conn := fullIp6ConnectPattern.FindStringSubmatch(dsn) if conn == nil { conn = fullConnectPattern.FindStringSubmatch(dsn) if conn == nil { // maybe it's a parameterized string that ParseConnectionString didn't like. for _, par := range fullParamPattern.FindAllStringSubmatch(dsn, -1) { if len(par) != 3 { continue } v := strings.Trim(par[2], "'") switch par[1] { case "dbname": cc.Database = v case "host": // don't overwrite if hostaddr already put a value here if cc.Host == "" { cc.Host = v } case "hostaddr": cc.Host = v case "port": cc.Port, _ = parsePort(v) } } if cc.Database == "" && cc.Host == "" && cc.Port == 0 { // Ok, we give up. return } } } if conn != nil { cc.Host = conn[3] cc.Database = conn[5] cc.Port, _ = parsePort(conn[4]) } } // default to the environment variables in case // we can't find them in the connection string var ppoid string if p, ok := cc.RuntimeParams["port"]; ok { cc.Port, ppoid = parsePort(p) } if cc.Port == 0 { cc.Port, ppoid = parsePort(getenv("PGPORT")) } else { ppoid = strconv.Itoa(int(cc.Port)) } // explicit hostaddr=xxx overrides the hostname if ha, ok := cc.RuntimeParams["hostaddr"]; ok { cc.Host = ha } if cc.Host == "" { cc.Host = getenv("PGHOST") } // The host string could have multiple comma-separated hosts, // which could have an attached ":port" at the end, but also // note that we can't get too anxious to jump on any colons in // the hostname string because the hostname could also be an IPv6 // address with optional port, as in // "[2001:db8:3333:4444:5555:6666:7777:8888]:5432", so we need // to look for that explicitly. cc.Host = strings.SplitN(cc.Host, ",", 2)[0] ip6parts := ip6HostPort.FindStringSubmatch(cc.Host) if ip6parts != nil { cc.Host = ip6parts[1] if ip6parts[2] != "" { cc.Port, ppoid = parsePort(ip6parts[2]) } } else { if colon := strings.IndexRune(cc.Host, ':'); colon >= 0 { // This host had an explicit port number attached to it. // Use that in preference to what's in cc.Port. if colon+1 < len(cc.Host) { cc.Port, ppoid = parsePort(cc.Host[colon+1:]) } cc.Host = cc.Host[0:colon] } } if cc.Database == "" { cc.Database = getenv("PGDATABASE") } // fill in the Postgres defaults explicitly if cc.Host == "" { cc.Host = "localhost" } if cc.Port == 0 { cc.Port, ppoid = parsePort("5432") } // Unix sockets are handled a little differently if strings.HasPrefix(cc.Host, "/") { ppoid = path.Join(cc.Host, ".s.PGSQL."+ppoid) cc.Host = "localhost" } s.Host = cc.Host s.PortPathOrID = ppoid s.DatabaseName = cc.Database } } go-agent-3.42.0/v3/integrations/nrpgx/nrpgx_test.go000066400000000000000000000144401510742411500222240ustar00rootroot00000000000000// Copyright 2020, 2021 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrpgx import ( "testing" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestParseDSN(t *testing.T) { testcases := []struct { dsn string expHost string expPortPathOrID string expDatabaseName string env map[string]string }{ // urls // #0 { dsn: "postgresql://", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, // #1 { dsn: "postgresql://localhost", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, // #2 { dsn: "postgresql://localhost:5433", expHost: "localhost", expPortPathOrID: "5433", expDatabaseName: "", }, // #3 { dsn: "postgresql://localhost/mydb", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "mydb", }, // #4 { dsn: "postgresql://user@localhost", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, // #5 { dsn: "postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "otherdb", }, // #6 { dsn: "postgresql:///mydb?host=myhost.com&port=5433", expHost: "myhost.com", expPortPathOrID: "5433", expDatabaseName: "mydb", }, // #7 { dsn: "postgresql://[2001:db8::1234]/database", expHost: "2001:db8::1234", expPortPathOrID: "5432", expDatabaseName: "database", }, // #8 { dsn: "postgresql://[2001:db8::1234]:7890/database", expHost: "2001:db8::1234", expPortPathOrID: "7890", expDatabaseName: "database", }, // #9 { dsn: "postgresql:///dbname?host=/var/lib/postgresql", expHost: "localhost", expPortPathOrID: "/var/lib/postgresql/.s.PGSQL.5432", expDatabaseName: "dbname", }, // #10 { dsn: "postgresql://%2Fvar%2Flib%2Fpostgresql/dbname", expHost: "", expPortPathOrID: "", expDatabaseName: "", }, // key,value pairs // #11 { dsn: "host=1.2.3.4 port=1234 dbname=mydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, // #12 { dsn: "host =1.2.3.4 port= 1234 dbname = mydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, // #13 { dsn: "host = 1.2.3.4 port=\t\t1234 dbname =\n\t\t\tmydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, // #14 { dsn: "host ='1.2.3.4' port= '1234' dbname = 'mydb'", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, // #15 { dsn: `host='ain\'t_single_quote' port='port\\slash' dbname='my db spaced'`, expHost: `ain\'t_single_quote`, expPortPathOrID: `5432`, expDatabaseName: "my db spaced", }, // #16 { dsn: `host=localhost port=so=does=this`, expHost: "localhost", expPortPathOrID: "5432", }, // #17 { dsn: "host=1.2.3.4 hostaddr=5.6.7.8", expHost: "5.6.7.8", expPortPathOrID: "5432", }, // #18 { dsn: "hostaddr=5.6.7.8 host=1.2.3.4", expHost: "5.6.7.8", expPortPathOrID: "5432", }, // #19 { dsn: "hostaddr=1.2.3.4", expHost: "1.2.3.4", expPortPathOrID: "5432", }, // #20 { dsn: "host=example.com,example.org port=80,443", expHost: "example.com", expPortPathOrID: "80", }, // #21 { dsn: "hostaddr=example.com,example.org port=80,443", expHost: "example.com", expPortPathOrID: "80", }, // #22 { dsn: "hostaddr='' host='' port=80,", expHost: "localhost", expPortPathOrID: "80", }, // #23 { dsn: "host=/path/to/socket", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.5432", }, // #24 { dsn: "port=1234 host=/path/to/socket", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.1234", }, // #25 { dsn: "host=/path/to/socket port=1234", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.1234", }, // env vars // #26 { dsn: "host=host_string port=port_string dbname=dbname_string", expHost: "host_string", expPortPathOrID: "5432", expDatabaseName: "dbname_string", env: map[string]string{ "PGHOST": "host_env", "PGPORT": "port_env", "PGDATABASE": "dbname_env", }, }, // #27 { dsn: "", expHost: "host_env", expPortPathOrID: "5432", expDatabaseName: "dbname_env", env: map[string]string{ "PGHOST": "host_env", "PGPORT": "port_env", "PGDATABASE": "dbname_env", }, }, // #28 { dsn: "host=host_string", expHost: "host_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOSTADDR": "hostaddr_env", }, }, // #29 { dsn: "hostaddr=hostaddr_string", expHost: "hostaddr_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOST": "host_env", }, }, // #30 { dsn: "host=host_string hostaddr=hostaddr_string", expHost: "hostaddr_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOST": "host_env", }, }, } for i, test := range testcases { getenv := func(env string) string { return test.env[env] } s := &newrelic.DatastoreSegment{} parseDSN(getenv)(s, test.dsn) if test.expHost != s.Host { t.Errorf(`testcase #%d, incorrect host, expected="%s", actual="%s"`, i, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`testcase #%d, incorrect port path or id, expected="%s", actual="%s"`, i, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`testcase #%d, incorrect database name, expected="%s", actual="%s"`, i, test.expDatabaseName, s.DatabaseName) } } } go-agent-3.42.0/v3/integrations/nrpgx5/000077500000000000000000000000001510742411500175625ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx5/LICENSE.txt000066400000000000000000000264501510742411500214140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx5/README.md000066400000000000000000000006611510742411500210440ustar00rootroot00000000000000# v3/integrations/nrpgx5 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5) Package `nrpgx5` instruments https://github.com/jackc/pgx/v5. ```go import "github.com/newrelic/go-agent/v3/integrations/nrpgx5" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5). go-agent-3.42.0/v3/integrations/nrpgx5/example/000077500000000000000000000000001510742411500212155ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx5/example/LICENSE.txt000066400000000000000000000264501510742411500230470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx5/example/pgx/000077500000000000000000000000001510742411500220135ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx5/example/pgx/LICENSE.txt000066400000000000000000000264501510742411500236450ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx5/example/pgx/main.go000066400000000000000000000026061510742411500232720ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "time" "github.com/jackc/pgx/v5" "github.com/newrelic/go-agent/v3/integrations/nrpgx5" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") if err != nil { panic(err) } cfg.Tracer = nrpgx5.NewTracer(nrpgx5.WithQueryParameters(true)) conn, err := pgx.ConnectConfig(context.Background(), cfg) if err != nil { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("PostgreSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // // N.B.: We do not recommend using app.WaitForConnection in production code. // app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("postgresQuery") ctx := newrelic.NewContext(context.Background(), txn) row := conn.QueryRow(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") count := 0 err = row.Scan(&count) if err != nil { log.Println(err) } var a, b int rows, _ := conn.Query(ctx, "select n, n*2 from generate_series(1, $1) n", 3) _, err = pgx.ForEachRow(rows, []any{&a, &b}, func() error { fmt.Printf("%v %v\n", a, b) return nil }) if err != nil { panic(err) } txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of entries in pg_catalog.pg_tables", count) } go-agent-3.42.0/v3/integrations/nrpgx5/example/pgxpool/000077500000000000000000000000001510742411500227055ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpgx5/example/pgxpool/LICENSE.txt000066400000000000000000000264501510742411500245370ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpgx5/example/pgxpool/main.go000066400000000000000000000025711510742411500241650ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/newrelic/go-agent/v3/integrations/nrpgx5" "github.com/newrelic/go-agent/v3/newrelic" ) func NewPgxPool(ctx context.Context, dbURL string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dbURL) if err != nil { return nil, err } cfg.BeforeConnect = func(_ context.Context, config *pgx.ConnConfig) error { config.Tracer = nrpgx5.NewTracer() return nil } return pgxpool.NewWithConfig(ctx, cfg) } func main() { db, err := NewPgxPool(context.Background(), "postgres://postgres:postgres@localhost:5432") if err != nil { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("PostgreSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // // N.B.: We do not recommend using app.WaitForConnection in production code. // app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("postgresQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRow(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") count := 0 err = row.Scan(&count) if err != nil { log.Println(err) } txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of entries in pg_catalog.pg_tables", count) } go-agent-3.42.0/v3/integrations/nrpgx5/go.mod000066400000000000000000000005021510742411500206650ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrpgx5 go 1.24 toolchain go1.24.2 require ( github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 github.com/jackc/pgx/v5 v5.5.4 github.com/newrelic/go-agent/v3 v3.42.0 github.com/stretchr/testify v1.8.1 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrpgx5/nrpgx5.go000066400000000000000000000200701510742411500213330ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrpgx5 instruments https://github.com/jackc/pgx/v5. // // Use this package to instrument your PostgreSQL calls using the pgx // library. // // This integration is specifically aimed at instrumenting applications which // use the pgx/v5 library to directly communicate with the Postgres database server // (i.e., not via the standard database/sql library). // // To instrument your database operations, you will need to call nrpgx5.NewTracer() to obtain // a pgx.Tracer value. You can do this either with a normal pgx.ParseConfig() call or the // pgxpool.ParseConfig() call if you wish to use pgx connection pools. // // For example: // // import ( // "github.com/jackc/pgx/v5" // "github.com/newrelic/go-agent/v3/integrations/nrpgx5" // "github.com/newrelic/go-agent/v3/newrelic" // ) // // func main() { // cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") // OR pgxpools.ParseConfig(...) // if err != nil { // panic(err) // } // // cfg.Tracer = nrpgx5.NewTracer() // conn, err := pgx.ConnectConfig(context.Background(), cfg) // if err != nil { // panic(err) // } // } // // See the programs in the example directory for working examples of each use case. package nrpgx5 import ( "context" "fmt" "strconv" "github.com/jackc/pgx/v5" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) func init() { internal.TrackUsage("integration", "driver", "nrpgx5") } type ( Tracer struct { BaseSegment newrelic.DatastoreSegment ParseQuery func(segment *newrelic.DatastoreSegment, query string) SendQueryParameters bool } nrPgxSegmentType string ) const ( querySegmentKey nrPgxSegmentType = "nrPgx5Segment" batchSegmentKey nrPgxSegmentType = "batchNrPgx5Segment" querySecurityKey nrPgxSegmentType = "nrPgx5SecurityToken" ) type TracerOption func(*Tracer) // NewTracer creates a new value which implements pgx.BatchTracer, pgx.ConnectTracer, pgx.PrepareTracer, and pgx.QueryTracer. // This value will be used to facilitate instrumentation of the database operations performed. // When establishing a connection to the database, the recommended usage is to do something like the following: // // cfg, err := pgx.ParseConfig("...") // if err != nil { ... } // cfg.Tracer = nrpgx5.NewTracer() // conn, err := pgx.ConnectConfig(context.Background(), cfg) // // If you do not wish to have SQL query parameters included in the telemetry data, add the WithQueryParameters // option, like so: // // cfg.Tracer = nrpgx5.NewTracer(nrpgx5.WithQueryParameters(false)) // // (The default is to collect query parameters, but you can explicitly select this by passing true to WithQueryParameters.) // // Note that query parameters may nevertheless be suppressed from the telemetry data due to agent configuration, // agent feature set, or policy independint of whether it's enabled here. func NewTracer(o ...TracerOption) *Tracer { t := &Tracer{ ParseQuery: sqlparse.ParseQuery, SendQueryParameters: true, } for _, opt := range o { opt(t) } return t } // WithQueryParameters is an option which may be passed to a call to NewTracer. It controls // whether or not to include the SQL query parameters in the telemetry data collected as part of // instrumenting database operations. // // By default this is enabled. To disable it, call NewTracer as NewTracer(WithQueryParameters(false)). // // Note that query parameters may nevertheless be suppressed from the telemetry data due to agent configuration, // agent feature set, or policy independint of whether it's enabled here. func WithQueryParameters(enabled bool) TracerOption { return func(t *Tracer) { t.SendQueryParameters = enabled } } // TraceConnectStart is called at the beginning of Connect and ConnectConfig calls, as // what is essentially a callback from the pgx/v5 library to us so we can trace the operation. // The returned context is used for // the rest of the call and will be passed to TraceConnectEnd. func (t *Tracer) TraceConnectStart(ctx context.Context, data pgx.TraceConnectStartData) context.Context { t.BaseSegment = newrelic.DatastoreSegment{ Product: newrelic.DatastorePostgres, Host: data.ConnConfig.Host, PortPathOrID: strconv.FormatUint(uint64(data.ConnConfig.Port), 10), DatabaseName: data.ConnConfig.Database, } return ctx } // TraceConnectEnd is called by pgx/v5 at the end of the Connect and ConnectConfig calls. func (Tracer) TraceConnectEnd(ctx context.Context, data pgx.TraceConnectEndData) {} // TraceQueryStart is called by pgx/v5 at the beginning of Query, QueryRow, and Exec calls. // The returned context is used for the // rest of the call and will be passed to TraceQueryEnd. // This starts a new datastore segment in the transaction stored in the passed context. func (t *Tracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { segment := t.BaseSegment segment.StartTime = newrelic.FromContext(ctx).StartSegmentNow() segment.ParameterizedQuery = data.SQL if t.SendQueryParameters { segment.QueryParameters = t.getQueryParameters(data.Args) } // fill Operation and Collection t.ParseQuery(&segment, data.SQL) if newrelic.IsSecurityAgentPresent() { stoken := newrelic.GetSecurityAgentInterface().SendEvent("SQL", data.SQL, data.Args) ctx = context.WithValue(ctx, querySecurityKey, stoken) } return context.WithValue(ctx, querySegmentKey, &segment) } // TraceQueryEnd is called by pgx/v5 at the completion of Query, QueryRow, and Exec calls. // This will terminate the datastore segment started when the database operation was started. func (t *Tracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) { segment, ok := ctx.Value(querySegmentKey).(*newrelic.DatastoreSegment) if !ok { return } if newrelic.IsSecurityAgentPresent() { if stoken := ctx.Value(querySecurityKey); stoken != nil { newrelic.GetSecurityAgentInterface().SendExitEvent(stoken, nil) } } segment.End() } func (t *Tracer) getQueryParameters(args []any) map[string]any { result := map[string]any{} for i, arg := range args { result["$"+strconv.Itoa(i)] = fmt.Sprintf("[%s]", arg) } return result } // TraceBatchStart is called at the beginning of SendBatch calls. The returned context is used for the // rest of the call and will be passed to TraceBatchQuery and TraceBatchEnd. func (t *Tracer) TraceBatchStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchStartData) context.Context { segment := t.BaseSegment segment.StartTime = newrelic.FromContext(ctx).StartSegmentNow() segment.Operation = "batch" segment.Collection = "" return context.WithValue(ctx, batchSegmentKey, &segment) } // TraceBatchQuery is called for each batched query operation. We will add the SQL statement to the segment's // ParameterizedQuery value. func (t *Tracer) TraceBatchQuery(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchQueryData) { segment, ok := ctx.Value(batchSegmentKey).(*newrelic.DatastoreSegment) if !ok { return } segment.ParameterizedQuery += data.SQL + "\n" } // TraceBatchEnd is called at the end of a batch. Here we will terminate the datastore segment we started when // the batch was started. func (t *Tracer) TraceBatchEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchEndData) { segment, ok := ctx.Value(batchSegmentKey).(*newrelic.DatastoreSegment) if !ok { return } segment.End() } // TracePrepareStart is called at the beginning of Prepare calls. The returned context is used for the // rest of the call and will be passed to TracePrepareEnd. // // The Query and QueryRow will call prepare, so here we don't do any additional work (otherwise // we'd duplicate segment data). func (t *Tracer) TracePrepareStart(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareStartData) context.Context { return ctx } // TracePrepareEnd implements pgx.PrepareTracer. func (t *Tracer) TracePrepareEnd(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareEndData) { } go-agent-3.42.0/v3/integrations/nrpgx5/nrpgx5_test.go000066400000000000000000000217531510742411500224030ustar00rootroot00000000000000package nrpgx5 import ( "context" "fmt" "net/url" "os" "strconv" "testing" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/stretchr/testify/assert" ) func TestTracer_Trace_CRUD(t *testing.T) { con, finish := getTestCon(t) defer finish() tests := []struct { name string fn func(context.Context, *pgx.Conn) metric []internal.WantMetric }{ { name: "query should send the metric after the row close", fn: func(ctx context.Context, con *pgx.Conn) { rows, _ := con.Query(ctx, "SELECT id, name, timestamp FROM mytable LIMIT $1", 2) rows.Close() }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, {Name: "Datastore/statement/Postgres/mytable/select"}, }, }, { name: "queryrow should send the metric after scan", fn: func(ctx context.Context, con *pgx.Conn) { row := con.QueryRow(ctx, "SELECT id, name, timestamp FROM mytable") _ = row.Scan() }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, {Name: "Datastore/statement/Postgres/mytable/select"}, }, }, { name: "insert should send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/insert"}, {Name: "Datastore/statement/Postgres/mytable/insert"}, }, }, { name: "update should send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Exec(ctx, "UPDATE mytable set name = $2 WHERE id = $1", 1, "myname is") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/update"}, {Name: "Datastore/statement/Postgres/mytable/update"}, }, }, { name: "delete should send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Exec(ctx, "DELETE FROM mytable WHERE id = $1", 4) }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/delete"}, {Name: "Datastore/statement/Postgres/mytable/delete"}, }, }, { name: "select 1 should send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Exec(ctx, "SELECT 1") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, }, }, { name: "query error should also send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Query(ctx, "SELECT * FROM non_existent_table") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, {Name: "Datastore/statement/Postgres/non_existent_table/select"}, }, }, { name: "exec error should also send the metric", fn: func(ctx context.Context, con *pgx.Conn) { _, _ = con.Exec(ctx, "INSERT INTO non_existent_table(name) VALUES ($1)", "wrong name") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/insert"}, {Name: "Datastore/statement/Postgres/non_existent_table/insert"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction(t.Name()) ctx := newrelic.NewContext(context.Background(), txn) tt.fn(ctx, con) txn.End() app.ExpectMetricsPresent(t, tt.metric) }) } } func TestTracer_connect(t *testing.T) { conn, finish := getTestCon(t) defer finish() cfg := conn.Config() tracer := cfg.Tracer.(*Tracer) // hostname will t.Run("connect should set tracer host port and database", func(t *testing.T) { assert.Equal(t, cfg.Host, tracer.BaseSegment.Host) assert.Equal(t, cfg.Database, tracer.BaseSegment.DatabaseName) assert.Equal(t, strconv.FormatUint(uint64(cfg.Port), 10), tracer.BaseSegment.PortPathOrID) }) t.Run("exec should send metric with instance host and port ", func(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction(t.Name()) ctx := newrelic.NewContext(context.Background(), txn) _, _ = conn.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") txn.End() app.ExpectMetricsPresent(t, []internal.WantMetric{ {Name: "Datastore/instance/Postgres/" + os.Getenv("PG_HOST") + "/" + tracer.BaseSegment.PortPathOrID}, }) }) } func TestTracer_batch(t *testing.T) { conn, finish := getTestCon(t) defer finish() cfg := conn.Config() tracer := cfg.Tracer.(*Tracer) t.Run("exec should send metric with instance host and port ", func(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction(t.Name()) ctx := newrelic.NewContext(context.Background(), txn) batch := &pgx.Batch{} _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name a") _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name b") _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name c") _ = batch.Queue("SELECT id FROM mytable ORDER by id DESC LIMIT 1") result := conn.SendBatch(ctx, batch) _ = result.Close() txn.End() app.ExpectMetricsPresent(t, []internal.WantMetric{ {Name: "Datastore/instance/Postgres/" + os.Getenv("PG_HOST") + "/" + tracer.BaseSegment.PortPathOrID}, {Name: "Datastore/operation/Postgres/batch"}, }) }) } func TestTracer_inPool(t *testing.T) { dsn := getDSN() cfg, _ := pgxpool.ParseConfig(dsn) cfg.ConnConfig.Tracer = NewTracer() u, _ := url.Parse(dsn) con, _ := pgxpool.NewWithConfig(context.Background(), cfg) tests := []struct { name string fn func(context.Context, *pgxpool.Pool) metric []internal.WantMetric }{ { name: "query should send the metric after the row close", fn: func(ctx context.Context, con *pgxpool.Pool) { rows, _ := con.Query(ctx, "SELECT id, name, timestamp FROM mytable LIMIT $1", 2) rows.Close() }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, {Name: "Datastore/statement/Postgres/mytable/select"}, }, }, { name: "queryrow should send the metric after scan", fn: func(ctx context.Context, con *pgxpool.Pool) { row := con.QueryRow(ctx, "SELECT id, name, timestamp FROM mytable") _ = row.Scan() }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, {Name: "Datastore/statement/Postgres/mytable/select"}, }, }, { name: "insert should send the metric", fn: func(ctx context.Context, con *pgxpool.Pool) { _, _ = con.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/insert"}, {Name: "Datastore/statement/Postgres/mytable/insert"}, }, }, { name: "update should send the metric", fn: func(ctx context.Context, con *pgxpool.Pool) { _, _ = con.Exec(ctx, "UPDATE mytable set name = $2 WHERE id = $1", 1, "myname is") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/update"}, {Name: "Datastore/statement/Postgres/mytable/update"}, }, }, { name: "delete should send the metric", fn: func(ctx context.Context, con *pgxpool.Pool) { _, _ = con.Exec(ctx, "DELETE FROM mytable WHERE id = $1", 4) }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/delete"}, {Name: "Datastore/statement/Postgres/mytable/delete"}, }, }, { name: "select 1 should send the metric", fn: func(ctx context.Context, con *pgxpool.Pool) { _, _ = con.Exec(ctx, "SELECT 1") }, metric: []internal.WantMetric{ {Name: "Datastore/operation/Postgres/select"}, }, }, { name: "metric should send the metric database instance", fn: func(ctx context.Context, con *pgxpool.Pool) { _, _ = con.Exec(ctx, "SELECT 1") }, metric: []internal.WantMetric{ {Name: "Datastore/instance/Postgres/" + os.Getenv("PG_HOST") + "/" + u.Port()}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { app := integrationsupport.NewBasicTestApp() txn := app.StartTransaction(t.Name()) ctx := newrelic.NewContext(context.Background(), txn) tt.fn(ctx, con) txn.End() app.ExpectMetricsPresent(t, tt.metric) }) } } func getTestCon(t *testing.T) (*pgx.Conn, func()) { dsn := getDSN() cfg, err := pgx.ParseConfig(dsn) if err != nil { t.Fatalf("failed to parse DSN: %v", err) t.Fail() } cfg.Tracer = NewTracer() con, err := pgx.ConnectConfig(context.Background(), cfg) if err != nil { t.Fatalf("failed to connect to database: %v", err) t.Fail() } return con, func() { err = con.Close(context.Background()) if err != nil { t.Errorf("failed to close database: %v", err) t.Fail() } } } func getDSN() string { pgHost := os.Getenv("PG_HOST") pgPort := os.Getenv("PG_PORT") pgUser := os.Getenv("PG_USER") pgPass := os.Getenv("PG_PW") pgDB := os.Getenv("PG_DB") pgParam := os.Getenv("PG_PARAM") dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s%s", pgUser, pgPass, pgHost, pgPort, pgDB, pgParam) return dsn } go-agent-3.42.0/v3/integrations/nrpgx5/pgsnap_tracer_batch.txt000066400000000000000000000045671510742411500243300ustar00rootroot00000000000000F {"Type":"Parse","Name":"stmtcache_9","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_9"} F {"Type":"Parse","Name":"stmtcache_10","Query":"SELECT id FROM mytable ORDER by id DESC LIMIT 1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_10"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[1043]} B {"Type":"NoData"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[]} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name a"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name b"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name c"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_10","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"BindComplete"} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1}]} B {"Type":"DataRow","Values":[{"binary":"00000008"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Terminate"} go-agent-3.42.0/v3/integrations/nrpgx5/pgsnap_tracer_connect.txt000066400000000000000000000014041510742411500246630ustar00rootroot00000000000000F {"Type":"Parse","Name":"stmtcache_8","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_8"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[1043]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_8","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Terminate"} go-agent-3.42.0/v3/integrations/nrpgx5/pgsnap_tracer_inpool.txt000066400000000000000000000154541510742411500245440ustar00rootroot00000000000000F {"Type":"Parse","Name":"stmtcache_11","Query":"SELECT id, name, timestamp FROM mytable LIMIT $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_11"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[20]} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_11","ParameterFormatCodes":[1],"Parameters":[{"binary":"0000000000000002"}],"ResultFormatCodes":[1,0,1]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 2"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_12","Query":"SELECT id, name, timestamp FROM mytable","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_12"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[]} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_12","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1,0,1]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"myname is"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000005"},{"text":"myname is"},{"binary":"00028ec50fdbabf2"}]} B {"Type":"DataRow","Values":[{"binary":"00000006"},{"text":"name a"},{"binary":"00028ec50fdbc3b3"}]} B {"Type":"DataRow","Values":[{"binary":"00000007"},{"text":"name b"},{"binary":"00028ec50fdbc3b3"}]} B {"Type":"DataRow","Values":[{"binary":"00000008"},{"text":"name c"},{"binary":"00028ec50fdbc3b3"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 7"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_13","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_13"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[1043]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_13","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_14","Query":"UPDATE mytable set name = $2 WHERE id = $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_14"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[23,1043]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_14","ParameterFormatCodes":[1,0],"Parameters":[{"binary":"00000001"},{"text":"myname is"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"UPDATE 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_15","Query":"DELETE FROM mytable WHERE id = $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_15"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[23]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_15","ParameterFormatCodes":[1],"Parameters":[{"binary":"00000004"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"DELETE 0"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Query","String":"SELECT 1"} B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} B {"Type":"DataRow","Values":[{"text":"1"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Query","String":"SELECT 1"} B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} B {"Type":"DataRow","Values":[{"text":"1"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} go-agent-3.42.0/v3/integrations/nrpgx5/pgsnap_tracer_trace_crud.txt000066400000000000000000000165041510742411500253540ustar00rootroot00000000000000F {"Type":"Parse","Name":"stmtcache_1","Query":"SELECT id, name, timestamp FROM mytable LIMIT $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_1"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[20]} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_1","ParameterFormatCodes":[1],"Parameters":[{"binary":"0000000000000002"}],"ResultFormatCodes":[1,0,1]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"Adrian"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 2"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_2","Query":"SELECT id, name, timestamp FROM mytable","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_2"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[]} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_2","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1,0,1]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"Adrian"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 3"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_3","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_3"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[1043]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_3","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_4","Query":"UPDATE mytable set name = $2 WHERE id = $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_4"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[23,1043]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_4","ParameterFormatCodes":[1,0],"Parameters":[{"binary":"00000001"},{"text":"myname is"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"UPDATE 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_5","Query":"DELETE FROM mytable WHERE id = $1","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_5"} F {"Type":"Sync"} B {"Type":"ParseComplete"} B {"Type":"ParameterDescription","ParameterOIDs":[23]} B {"Type":"NoData"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_5","ParameterFormatCodes":[1],"Parameters":[{"binary":"00000004"}],"ResultFormatCodes":[]} F {"Type":"Describe","ObjectType":"P","Name":""} F {"Type":"Execute","Portal":"","MaxRows":0} F {"Type":"Sync"} B {"Type":"BindComplete"} B {"Type":"NoData"} B {"Type":"CommandComplete","CommandTag":"DELETE 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Query","String":"SELECT 1"} B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} B {"Type":"DataRow","Values":[{"text":"1"}]} B {"Type":"CommandComplete","CommandTag":"SELECT 1"} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_6","Query":"SELECT * FROM non_existent_table","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_6"} F {"Type":"Sync"} B {"Type":"ErrorResponse","Severity":"ERROR","SeverityUnlocalized":"ERROR","Code":"42P01","Message":"relation \"non_existent_table\" does not exist","Detail":"","Hint":"","Position":15,"InternalPosition":0,"InternalQuery":"","Where":"","SchemaName":"","TableName":"","ColumnName":"","DataTypeName":"","ConstraintName":"","File":"parse_relation.c","Line":1384,"Routine":"parserOpenTable","UnknownFields":null} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Parse","Name":"stmtcache_7","Query":"INSERT INTO non_existent_table(name) VALUES ($1)","ParameterOIDs":null} F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_7"} F {"Type":"Sync"} B {"Type":"ErrorResponse","Severity":"ERROR","SeverityUnlocalized":"ERROR","Code":"42P01","Message":"relation \"non_existent_table\" does not exist","Detail":"","Hint":"","Position":13,"InternalPosition":0,"InternalQuery":"","Where":"","SchemaName":"","TableName":"","ColumnName":"","DataTypeName":"","ConstraintName":"","File":"parse_relation.c","Line":1384,"Routine":"parserOpenTable","UnknownFields":null} B {"Type":"ReadyForQuery","TxStatus":"I"} F {"Type":"Terminate"} go-agent-3.42.0/v3/integrations/nrpkgerrors/000077500000000000000000000000001510742411500207155ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpkgerrors/LICENSE.txt000066400000000000000000000264501510742411500225470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpkgerrors/README.md000066400000000000000000000007271510742411500222020ustar00rootroot00000000000000# v3/integrations/nrpkgerrors [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpkgerrors?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpkgerrors) Package `nrpkgerrors` introduces support for https://github.com/pkg/errors. ```go import "github.com/newrelic/go-agent/v3/integrations/nrpkgerrors" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpkgerrors). go-agent-3.42.0/v3/integrations/nrpkgerrors/example/000077500000000000000000000000001510742411500223505ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpkgerrors/example/main.go000066400000000000000000000020551510742411500236250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "time" "github.com/newrelic/go-agent/v3/integrations/nrpkgerrors" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" ) type sampleError string func (e sampleError) Error() string { return string(e) } func alpha() error { return errors.WithStack(sampleError("alpha is the cause")) } func beta() error { return errors.WithStack(alpha()) } func gamma() error { return errors.Wrap(beta(), "gamma was involved") } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("pkg/errors App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } txn := app.StartTransaction("has-error") e := gamma() txn.NoticeError(nrpkgerrors.Wrap(e)) txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrpkgerrors/example_test.go000066400000000000000000000017101510742411500237350ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrpkgerrors_test import ( "github.com/newrelic/go-agent/v3/integrations/nrpkgerrors" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" ) type rootError string func (e rootError) Error() string { return string(e) } func makeRootError() error { return errors.WithStack(rootError("this is the original error")) } func Example() { var txn *newrelic.Transaction e := errors.Wrap(makeRootError(), "extra information") // Wrap the error to record stack-trace and class type information from // the error's root cause. Here, "rootError" will be recored as the // class and top stack-trace frame will be inside makeRootError(). // Without nrpkgerrors.Wrap, "*errors.withStack" would be recorded as // the class and the top stack-trace frame would be site of the // NoticeError call. txn.NoticeError(nrpkgerrors.Wrap(e)) } go-agent-3.42.0/v3/integrations/nrpkgerrors/go.mod000066400000000000000000000006641510742411500220310ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrpkgerrors // As of Dec 2019, 1.11 is the earliest version of Go tested by pkg/errors: // https://github.com/pkg/errors/blob/master/.travis.yml go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 // v0.8.0 was the last release in 2016, and when // major development on pkg/errors stopped. github.com/pkg/errors v0.8.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrpkgerrors/nrkpgerrors_test.go000066400000000000000000000104031510742411500246570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrpkgerrors import ( "runtime" "strings" "testing" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" ) func topFrameFunction(stack []uintptr) string { var frame runtime.Frame frames := runtime.CallersFrames(stack) if nil != frames { frame, _ = frames.Next() } return frame.Function } type basicError struct{} func (e basicError) Error() string { return "something went wrong" } func alpha(e error) error { return errors.WithStack(e) } func beta(e error) error { return errors.WithStack(e) } func gamma(e error) error { return errors.WithStack(e) } func theta(e error) error { return errors.WithMessage(e, "theta") } func basicNRError(e error) newrelic.Error { return newrelic.Error{Message: e.Error()} } func withAttributes(e error) newrelic.Error { return newrelic.Error{ Message: e.Error(), Attributes: map[string]interface{}{ "testAttribute": 1, "foo": 2, }, } } func TestWrappedStackTrace(t *testing.T) { testcases := []struct { Error error ExpectTopFrame string ExpectAttributes map[string]interface{} }{ {Error: basicError{}, ExpectTopFrame: ""}, {Error: alpha(basicError{}), ExpectTopFrame: "alpha"}, {Error: alpha(beta(gamma(basicError{}))), ExpectTopFrame: "gamma"}, {Error: alpha(theta(basicError{})), ExpectTopFrame: "alpha"}, {Error: alpha(theta(beta(basicError{}))), ExpectTopFrame: "beta"}, {Error: alpha(theta(beta(theta(basicError{})))), ExpectTopFrame: "beta"}, {Error: theta(basicError{}), ExpectTopFrame: ""}, {Error: basicNRError(basicError{}), ExpectTopFrame: ""}, {Error: withAttributes(basicError{}), ExpectTopFrame: "", ExpectAttributes: map[string]interface{}{"testAttribute": 1, "foo": 2}}, {Error: nil, ExpectTopFrame: ""}, } for idx, tc := range testcases { e := Wrap(tc.Error) st := e.(newrelic.Error).StackTrace() fn := topFrameFunction(st) if !strings.Contains(fn, tc.ExpectTopFrame) { t.Errorf("testcase %d: expected %s got %s", idx, tc.ExpectTopFrame, fn) } // check that error attributes are equal if they are expected if tc.ExpectAttributes != nil { errorAttributes := e.(newrelic.Error).ErrorAttributes() if len(tc.ExpectAttributes) != len(errorAttributes) { t.Errorf("testcase %d: error attribute size expected %d got %d", idx, len(tc.ExpectAttributes), len(errorAttributes)) } for k, v := range errorAttributes { if tc.ExpectAttributes[k] != v { t.Errorf("testcase %d: expected attribute %s:%v got %s:%v", idx, k, tc.ExpectAttributes[k], k, v) } } } } } type withClass struct{ class string } func errorWithClass(class string) error { return withClass{class: class} } func (e withClass) Error() string { return "something went wrong" } func (e withClass) ErrorClass() string { return e.class } type classAndCause struct { cause error class string } func wrapWithClass(e error, class string) error { return classAndCause{cause: e, class: class} } func (e classAndCause) Error() string { return e.cause.Error() } func (e classAndCause) Cause() error { return e.cause } func (e classAndCause) ErrorClass() string { return e.class } func TestWrappedErrorClass(t *testing.T) { // First choice is any ErrorClass of the immediate error. // Second choice is any ErrorClass of the error's cause. // Final choice is the reflect type of the error's cause. testcases := []struct { Error error ExpectClass string }{ {Error: basicError{}, ExpectClass: "nrpkgerrors.basicError"}, {Error: errorWithClass("zap"), ExpectClass: "zap"}, {Error: wrapWithClass(errorWithClass("zap"), "zip"), ExpectClass: "zip"}, {Error: theta(wrapWithClass(errorWithClass("zap"), "zip")), ExpectClass: "zap"}, {Error: alpha(basicError{}), ExpectClass: "nrpkgerrors.basicError"}, {Error: wrapWithClass(basicError{}, "zip"), ExpectClass: "zip"}, {Error: alpha(wrapWithClass(basicError{}, "zip")), ExpectClass: "nrpkgerrors.basicError"}, {Error: nil, ExpectClass: "*errors.fundamental"}, } for idx, tc := range testcases { e := Wrap(tc.Error) class := e.(newrelic.Error).ErrorClass() if class != tc.ExpectClass { t.Errorf("testcase %d: expected %s got %s", idx, tc.ExpectClass, class) } } } go-agent-3.42.0/v3/integrations/nrpkgerrors/nrpkgerrors.go000066400000000000000000000045461510742411500236330ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrpkgerrors introduces support for https://github.com/pkg/errors. // // This package improves the class and stack-trace fields of pkg/error errors // when they are recorded with Transaction.NoticeError. package nrpkgerrors import ( "fmt" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" ) func init() { internal.TrackUsage("integration", "pkg-errors") } // stackTracer is an error that also knows about its StackTrace. // All wrapped errors from github.com/pkg/errors implement this interface. type stackTracer interface { StackTrace() errors.StackTrace } func deepestStackTrace(err error) errors.StackTrace { var last stackTracer for err != nil { if err, ok := err.(stackTracer); ok { last = err } cause, ok := err.(interface { Cause() error }) if !ok { break } err = cause.Cause() } if last == nil { return nil } return last.StackTrace() } func transformStackTrace(orig errors.StackTrace) []uintptr { st := make([]uintptr, len(orig)) for i, frame := range orig { st[i] = uintptr(frame) } return st } func stackTrace(e error) []uintptr { st := deepestStackTrace(e) if nil == st { return nil } return transformStackTrace(st) } type errorClasser interface { ErrorClass() string } func errorClass(e error) string { if ec, ok := e.(errorClasser); ok { return ec.ErrorClass() } cause := errors.Cause(e) if ec, ok := cause.(errorClasser); ok { return ec.ErrorClass() } return fmt.Sprintf("%T", cause) } var ( errNilError = errors.New("nil") ) // Wrap wraps a pkg/errors error so that when noticed by // newrelic.Transaction.NoticeError it gives an improved stacktrace and class // type. func Wrap(e error) error { if e == nil { return newrelic.Error{ Message: errNilError.Error(), Class: errorClass(errNilError), Stack: stackTrace(errNilError), } } attributes := make(map[string]interface{}) switch error := e.(type) { case newrelic.Error: // if e is type newrelic.Error, copy attributes into wrapped error for key, value := range error.ErrorAttributes() { attributes[key] = value } } return newrelic.Error{ Message: e.Error(), Class: errorClass(e), Stack: stackTrace(e), Attributes: attributes, } } go-agent-3.42.0/v3/integrations/nrpq/000077500000000000000000000000001510742411500173175ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpq/LICENSE.txt000066400000000000000000000264501510742411500211510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpq/README.md000066400000000000000000000006361510742411500206030ustar00rootroot00000000000000# v3/integrations/nrpq [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq) Package `nrpq` instruments https://github.com/lib/pq. ```go import "github.com/newrelic/go-agent/v3/integrations/nrpq" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq). go-agent-3.42.0/v3/integrations/nrpq/example/000077500000000000000000000000001510742411500207525ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpq/example/main.go000066400000000000000000000032251510742411500222270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // To run this example, be sure the environment variable NEW_RELIC_LICENSE_KEY // is set to your license key. Postgres must be running on the default port // 5432 on localhost, and have a password "docker". An easy (albeit insecure) // way to test this is to issue the following command to run a postgres database // in a docker container: // docker run --rm -e POSTGRES_PASSWORD=docker -p 5432:5432 postgres // // Run that in the background or in a separate window, and then run this program // to access that database. // package main import ( "context" "database/sql" "fmt" "os" "time" _ "github.com/newrelic/go-agent/v3/integrations/nrpq" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // docker run --rm -e POSTGRES_PASSWORD=docker -p 5432:5432 postgres db, err := sql.Open("nrpostgres", "host=localhost port=5432 user=postgres dbname=postgres password=docker sslmode=disable") if err != nil { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("PostgreSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDatastoreRawQuery(true), ) if nil != err { panic(err) } app.WaitForConnection(5 * time.Second) txn := app.StartTransaction("postgresQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") var count int row.Scan(&count) txn.End() app.Shutdown(5 * time.Second) fmt.Println("number of entries in pg_catalog.pg_tables", count) } go-agent-3.42.0/v3/integrations/nrpq/example/sqlx/000077500000000000000000000000001510742411500217415ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrpq/example/sqlx/LICENSE.txt000066400000000000000000000264501510742411500235730ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrpq/example/sqlx/go.mod000066400000000000000000000007301510742411500230470ustar00rootroot00000000000000// This sqlx example is a separate module to avoid adding sqlx dependency to the // nrpq go.mod file. module github.com/newrelic/go-agent/v3/integrations/nrpq/example/sqlx go 1.24 require ( github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.1.0 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrpq v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrpq => ../../ replace github.com/newrelic/go-agent/v3 => ../../../.. go-agent-3.42.0/v3/integrations/nrpq/example/sqlx/main.go000066400000000000000000000111551510742411500232170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // An application that illustrates how to instrument jmoiron/sqlx with DatastoreSegments // // To run this example, be sure the environment varible NEW_RELIC_LICENSE_KEY // is set to your license key. Postgres must be running on the default port // 5432 and have a user "foo" and a database "bar". One quick (albeit insecure) // way of doing this is to run a small local Postgres instance in Docker: // // docker run --rm -e POSTGRES_USER=foo -e POSTGRES_DB=bar \ // -e POSTGRES_PASSWORD=password -e POSTGRES_HOST_AUTH_METHOD=trust \ // -p 5432:5432 postgres & // // Adding instrumentation for the SQLx package is easy. It means you can // make database calls without having to manually create DatastoreSegments. // Setup can be done in two steps: // // # Set up your driver // // If you are using one of our currently supported database drivers (see // https://docs.newrelic.com/docs/agents/go-agent/get-started/go-agent-compatibility-requirements#frameworks), // follow the instructions on installing the driver. // // As an example, for the `lib/pq` driver, you will use the newrelic // integration's driver in place of the postgres driver. If your code is using // sqlx.Open with `lib/pq` like this: // // import ( // "github.com/jmoiron/sqlx" // _ "github.com/lib/pq" // ) // // func main() { // db, err := sqlx.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // Then change the side-effect import to the integration package, and open // "nrpostgres" instead: // // import ( // "github.com/jmoiron/sqlx" // _ "github.com/newrelic/go-agent/v3/integrations/nrpq" // ) // // func main() { // db, err := sqlx.Open("nrpostgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // If you are not using one of the supported database drivers, use the // `InstrumentSQLDriver` // (https://godoc.org/github.com/newrelic/go-agent#InstrumentSQLDriver) API. // See // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // for a full example. // // # Add context to your database calls // // Next, you must provide a context containing a newrelic.Transaction to all // methods on sqlx.DB, sqlx.NamedStmt, sqlx.Stmt, and sqlx.Tx that make a // database call. For example, instead of the following: // // err := db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // err := db.GetContext(ctx, &jason, "SELECT * FROM person WHERE first_name=$1", "Jason") package main import ( "context" "log" "os" "time" "github.com/jmoiron/sqlx" _ "github.com/newrelic/go-agent/v3/integrations/nrpq" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) var schema = ` CREATE TABLE person ( first_name text, last_name text, email text )` // Person is a person in the database type Person struct { FirstName string `db:"first_name"` LastName string `db:"last_name"` Email string } func createApp() *newrelic.Application { app, err := newrelic.NewApplication( newrelic.ConfigAppName("SQLx"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { log.Fatalln(err) } if err := app.WaitForConnection(5 * time.Second); nil != err { log.Fatalln(err) } return app } func main() { // Create application app := createApp() defer app.Shutdown(10 * time.Second) // Start a transaction txn := app.StartTransaction("main") defer txn.End() // Add transaction to context ctx := newrelic.NewContext(context.Background(), txn) // Connect to database using the "nrpostgres" driver db, err := sqlx.Connect("nrpostgres", "user=foo dbname=bar sslmode=disable") if err != nil { log.Fatalln(err) } // Create database table if it does not exist already // When the context is passed, DatastoreSegments will be created db.ExecContext(ctx, schema) // Add people to the database // When the context is passed, DatastoreSegments will be created tx := db.MustBegin() tx.MustExecContext(ctx, "INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "Jason", "Moiron", "jmoiron@jmoiron.net") tx.MustExecContext(ctx, "INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "John", "Doe", "johndoeDNE@gmail.net") tx.Commit() // Read from the database // When the context is passed, DatastoreSegments will be created people := []Person{} db.SelectContext(ctx, &people, "SELECT * FROM person ORDER BY first_name ASC") jason := Person{} db.GetContext(ctx, &jason, "SELECT * FROM person WHERE first_name=$1", "Jason") } go-agent-3.42.0/v3/integrations/nrpq/go.mod000066400000000000000000000005001510742411500204200ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrpq go 1.24 require ( // NewConnector dsn parsing tests expect v1.1.0 error return behavior. github.com/lib/pq v1.1.0 // v3.3.0 includes the new location of ParseQuery github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrpq/nrpq.go000066400000000000000000000104011510742411500206220ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 // Package nrpq instruments https://github.com/lib/pq. // // Use this package to instrument your PostgreSQL calls without having to manually // create DatastoreSegments. This is done in a two step process: // // 1. Use this package's driver in place of the postgres driver. // // If your code is using sql.Open like this: // // import ( // _ "github.com/lib/pq" // ) // // func main() { // db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // Then change the side-effect import to this package, and open "nrpostgres" instead: // // import ( // _ "github.com/newrelic/go-agent/v3/integrations/nrpq" // ) // // func main() { // db, err := sql.Open("nrpostgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") // } // // If your code is using pq.NewConnector, simply use nrpq.NewConnector // instead. // // 2. Provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, and sql.Tx. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) FROM pg_catalog.pg_tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") // // Unfortunately, sql.Stmt exec and query calls are not supported since pq.stmt // does not have ExecContext and QueryContext methods (as of June 2019, see // https://github.com/lib/pq/pull/768). // // A working example is shown here: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrpq/example/main.go package nrpq import ( "database/sql" "database/sql/driver" "os" "path" "regexp" "strings" "github.com/lib/pq" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastorePostgres, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN(os.Getenv), } ) // NewConnector can be used in place of pq.NewConnector to get an instrumented // PostgreSQL connector. func NewConnector(dsn string) (driver.Connector, error) { connector, err := pq.NewConnector(dsn) if nil != err || nil == connector { // Return nil rather than 'connector' since a nil pointer would // be returned as a non-nil driver.Connector. return nil, err } bld := baseBuilder bld.ParseDSN(&bld.BaseSegment, dsn) return newrelic.InstrumentSQLConnector(connector, bld), nil } func init() { sql.Register("nrpostgres", newrelic.InstrumentSQLDriver(&pq.Driver{}, baseBuilder)) internal.TrackUsage("integration", "driver", "postgres") } var dsnSplit = regexp.MustCompile(`(\w+)\s*=\s*('[^=]*'|[^'\s]+)`) func getFirstHost(value string) string { host := strings.SplitN(value, ",", 2)[0] host = strings.Trim(host, "[]") return host } func parseDSN(getenv func(string) string) func(*newrelic.DatastoreSegment, string) { return func(s *newrelic.DatastoreSegment, dsn string) { if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { var err error dsn, err = pq.ParseURL(dsn) if nil != err { return } } host := getenv("PGHOST") hostaddr := "" ppoid := getenv("PGPORT") dbname := getenv("PGDATABASE") for _, split := range dsnSplit.FindAllStringSubmatch(dsn, -1) { if len(split) != 3 { continue } key := split[1] value := strings.Trim(split[2], `'`) switch key { case "dbname": dbname = value case "host": host = getFirstHost(value) case "hostaddr": hostaddr = getFirstHost(value) case "port": ppoid = strings.SplitN(value, ",", 2)[0] } } if "" != hostaddr { host = hostaddr } else if "" == host { host = "localhost" } if "" == ppoid { ppoid = "5432" } if strings.HasPrefix(host, "/") { // this is a unix socket ppoid = path.Join(host, ".s.PGSQL."+ppoid) host = "localhost" } s.Host = host s.PortPathOrID = ppoid s.DatabaseName = dbname } } go-agent-3.42.0/v3/integrations/nrpq/nrpq_test.go000066400000000000000000000147621510742411500216770ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrpq import ( "testing" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestParseDSN(t *testing.T) { testcases := []struct { dsn string expHost string expPortPathOrID string expDatabaseName string env map[string]string }{ // urls { dsn: "postgresql://", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, { dsn: "postgresql://localhost", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, { dsn: "postgresql://localhost:5433", expHost: "localhost", expPortPathOrID: "5433", expDatabaseName: "", }, { dsn: "postgresql://localhost/mydb", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "mydb", }, { dsn: "postgresql://user@localhost", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "", }, { dsn: "postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp", expHost: "localhost", expPortPathOrID: "5432", expDatabaseName: "otherdb", }, { dsn: "postgresql:///mydb?host=myhost.com&port=5433", expHost: "myhost.com", expPortPathOrID: "5433", expDatabaseName: "mydb", }, { dsn: "postgresql://[2001:db8::1234]/database", expHost: "2001:db8::1234", expPortPathOrID: "5432", expDatabaseName: "database", }, { dsn: "postgresql://[2001:db8::1234]:7890/database", expHost: "2001:db8::1234", expPortPathOrID: "7890", expDatabaseName: "database", }, { dsn: "postgresql:///dbname?host=/var/lib/postgresql", expHost: "localhost", expPortPathOrID: "/var/lib/postgresql/.s.PGSQL.5432", expDatabaseName: "dbname", }, { dsn: "postgresql://%2Fvar%2Flib%2Fpostgresql/dbname", expHost: "", expPortPathOrID: "", expDatabaseName: "", }, // key,value pairs { dsn: "host=1.2.3.4 port=1234 dbname=mydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, { dsn: "host =1.2.3.4 port= 1234 dbname = mydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, { dsn: "host = 1.2.3.4 port=\t\t1234 dbname =\n\t\t\tmydb", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, { dsn: "host ='1.2.3.4' port= '1234' dbname = 'mydb'", expHost: "1.2.3.4", expPortPathOrID: "1234", expDatabaseName: "mydb", }, { dsn: `host='ain\'t_single_quote' port='port\\slash' dbname='my db spaced'`, expHost: `ain\'t_single_quote`, expPortPathOrID: `port\\slash`, expDatabaseName: "my db spaced", }, { dsn: `host=localhost port=so=does=this`, expHost: "localhost", expPortPathOrID: "so=does=this", }, { dsn: "host=1.2.3.4 hostaddr=5.6.7.8", expHost: "5.6.7.8", expPortPathOrID: "5432", }, { dsn: "hostaddr=5.6.7.8 host=1.2.3.4", expHost: "5.6.7.8", expPortPathOrID: "5432", }, { dsn: "hostaddr=1.2.3.4", expHost: "1.2.3.4", expPortPathOrID: "5432", }, { dsn: "host=example.com,example.org port=80,443", expHost: "example.com", expPortPathOrID: "80", }, { dsn: "hostaddr=example.com,example.org port=80,443", expHost: "example.com", expPortPathOrID: "80", }, { dsn: "hostaddr='' host='' port=80,", expHost: "localhost", expPortPathOrID: "80", }, { dsn: "host=/path/to/socket", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.5432", }, { dsn: "port=1234 host=/path/to/socket", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.1234", }, { dsn: "host=/path/to/socket port=1234", expHost: "localhost", expPortPathOrID: "/path/to/socket/.s.PGSQL.1234", }, // env vars { dsn: "host=host_string port=port_string dbname=dbname_string", expHost: "host_string", expPortPathOrID: "port_string", expDatabaseName: "dbname_string", env: map[string]string{ "PGHOST": "host_env", "PGPORT": "port_env", "PGDATABASE": "dbname_env", }, }, { dsn: "", expHost: "host_env", expPortPathOrID: "port_env", expDatabaseName: "dbname_env", env: map[string]string{ "PGHOST": "host_env", "PGPORT": "port_env", "PGDATABASE": "dbname_env", }, }, { dsn: "host=host_string", expHost: "host_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOSTADDR": "hostaddr_env", }, }, { dsn: "hostaddr=hostaddr_string", expHost: "hostaddr_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOST": "host_env", }, }, { dsn: "host=host_string hostaddr=hostaddr_string", expHost: "hostaddr_string", expPortPathOrID: "5432", env: map[string]string{ "PGHOST": "host_env", }, }, } for _, test := range testcases { getenv := func(env string) string { return test.env[env] } s := &newrelic.DatastoreSegment{} parseDSN(getenv)(s, test.dsn) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } func TestNewConnector(t *testing.T) { connector, err := NewConnector("client_encoding=") if err == nil { t.Error("error expected from invalid dsn") } if connector != nil { t.Error("nil connector expected from invalid dsn") } connector, err = NewConnector("host=localhost port=5432 user=postgres dbname=postgres password=docker sslmode=disable") if err != nil { t.Error("nil error expected from valid dsn", err) } if connector == nil { t.Error("non-nil connector expected from valid dsn") } } go-agent-3.42.0/v3/integrations/nrredis-v7/000077500000000000000000000000001510742411500203375ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v7/LICENSE.txt000066400000000000000000000264501510742411500221710ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrredis-v7/README.md000066400000000000000000000007161510742411500216220ustar00rootroot00000000000000# v3/integrations/nrredis-v7 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7) Package `nrredis` instruments `"github.com/go-redis/redis/v7"`. ```go import nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v7" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v7). go-agent-3.42.0/v3/integrations/nrredis-v7/example/000077500000000000000000000000001510742411500217725ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v7/example/main.go000066400000000000000000000022711510742411500232470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "os" "time" redis "github.com/go-redis/redis/v7" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v7" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Redis App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("ping txn") opts := &redis.Options{ Addr: "localhost:6379", } client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context which includes // the transaction. // ctx := newrelic.NewContext(context.Background(), txn) pipe := client.WithContext(ctx).Pipeline() incr := pipe.Incr("pipeline_counter") pipe.Expire("pipeline_counter", time.Hour) _, err = pipe.Exec() fmt.Println(incr.Val(), err) txn.End() app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nrredis-v7/go.mod000066400000000000000000000004251510742411500214460ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrredis-v7 // https://github.com/go-redis/redis/blob/master/go.mod go 1.24 require ( github.com/go-redis/redis/v7 v7.0.0-beta.5 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrredis-v7/nrredis.go000066400000000000000000000053341510742411500223410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrredis instruments github.com/go-redis/redis/v7. // // Use this package to instrument your go-redis/redis/v7 calls without having to // manually create DatastoreSegments. package nrredis import ( "context" "net" "strings" redis "github.com/go-redis/redis/v7" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "datastore", "redis") } type contextKeyType struct{} type hook struct { segment newrelic.DatastoreSegment } var ( segmentContextKey = contextKeyType(struct{}{}) ) // NewHook creates a redis.Hook to instrument Redis calls. Add it to your // client, then ensure that all calls contain a context which includes the // transaction. The options are optional. Provide them to get instance metrics // broken out by host and port. The hook returned can be used with // redis.Client, redis.ClusterClient, and redis.Ring. func NewHook(opts *redis.Options) redis.Hook { h := hook{} h.segment.Product = newrelic.DatastoreRedis if opts != nil { // Per https://godoc.org/github.com/go-redis/redis#Options the // network should either be tcp or unix, and the default is tcp. if opts.Network == "unix" { h.segment.Host = "localhost" h.segment.PortPathOrID = opts.Addr } else if host, port, err := net.SplitHostPort(opts.Addr); err == nil { if "" == host { host = "localhost" } h.segment.Host = host h.segment.PortPathOrID = port } } return h } func (h hook) before(ctx context.Context, operation string) (context.Context, error) { txn := newrelic.FromContext(ctx) if txn == nil { return ctx, nil } s := h.segment s.StartTime = txn.StartSegmentNow() s.Operation = operation ctx = context.WithValue(ctx, segmentContextKey, &s) return ctx, nil } func (h hook) after(ctx context.Context) { if segment, ok := ctx.Value(segmentContextKey).(interface{ End() }); ok { segment.End() } } func (h hook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { return h.before(ctx, cmd.Name()) } func (h hook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { h.after(ctx) return nil } func pipelineOperation(cmds []redis.Cmder) string { operations := make([]string, 0, len(cmds)) for _, cmd := range cmds { operations = append(operations, cmd.Name()) } return "pipeline:" + strings.Join(operations, ",") } func (h hook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { return h.before(ctx, pipelineOperation(cmds)) } func (h hook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { h.after(ctx) return nil } go-agent-3.42.0/v3/integrations/nrredis-v7/nrredis_example_test.go000066400000000000000000000025571510742411500251170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis_test import ( "context" "fmt" redis "github.com/go-redis/redis/v7" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v7" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func getTransaction() *newrelic.Transaction { return nil } func Example_client() { opts := &redis.Options{Addr: "localhost:6379"} client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.WithContext(ctx).Ping().Result() fmt.Println(pong, err) } func Example_clusterClient() { client := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, }) // // Step 1: Add a nrredis.NewHook() to your redis cluster client. // client.AddHook(nrredis.NewHook(nil)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.WithContext(ctx).Ping().Result() fmt.Println(pong, err) } go-agent-3.42.0/v3/integrations/nrredis-v7/nrredis_test.go000066400000000000000000000111201510742411500233660ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis import ( "context" "net" "testing" redis "github.com/go-redis/redis/v7" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func emptyDialer(context.Context, string, string) (net.Conn, error) { return &net.TCPConn{}, nil } func TestPing(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(nil)) client.WithContext(ctx).Ping() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, }) } func TestPingWithOptionsAndAddress(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(opts)) client.WithContext(ctx).Ping() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/instance/Redis/myhost/myport", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, }) } func TestPipelineOperation(t *testing.T) { // As of Jan 16, 2020, it is impossible to test pipeline operations using // a &net.TCPConn{}, so we will have to make do with this. if op := pipelineOperation(nil); op != "pipeline:" { t.Error(op) } cmds := []redis.Cmder{redis.NewCmd("GET"), redis.NewCmd("SET")} if op := pipelineOperation(cmds); op != "pipeline:get,set" { t.Error(op) } } func TestNewHookAddress(t *testing.T) { testcases := []struct { network string address string expHost string expPort string }{ // examples from net.Dial https://godoc.org/net#Dial { network: "tcp", address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "", // tcp is assumed if missing address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "tcp", address: "192.0.2.1:http", expHost: "192.0.2.1", expPort: "http", }, { network: "tcp", address: "198.51.100.1:80", expHost: "198.51.100.1", expPort: "80", }, { network: "tcp", address: ":80", expHost: "localhost", expPort: "80", }, { network: "tcp", address: "0.0.0.0:80", expHost: "0.0.0.0", expPort: "80", }, { network: "tcp", address: "[::]:80", expHost: "::", expPort: "80", }, { network: "unix", address: "path/to/socket", expHost: "localhost", expPort: "path/to/socket", }, } for _, tc := range testcases { t.Run(tc.network+","+tc.address, func(t *testing.T) { hk := NewHook(&redis.Options{ Network: tc.network, Addr: tc.address, }).(hook) if hk.segment.Host != tc.expHost { t.Errorf("incorrect host: expect=%s actual=%s", tc.expHost, hk.segment.Host) } if hk.segment.PortPathOrID != tc.expPort { t.Errorf("incorrect port: expect=%s actual=%s", tc.expPort, hk.segment.PortPathOrID) } }) } } go-agent-3.42.0/v3/integrations/nrredis-v8/000077500000000000000000000000001510742411500203405ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v8/LICENSE.txt000066400000000000000000000264501510742411500221720ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrredis-v8/README.md000066400000000000000000000007161510742411500216230ustar00rootroot00000000000000# v3/integrations/nrredis-v8 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8) Package `nrredis` instruments `"github.com/go-redis/redis/v8"`. ```go import nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8). go-agent-3.42.0/v3/integrations/nrredis-v8/example/000077500000000000000000000000001510742411500217735ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v8/example/main.go000066400000000000000000000037751510742411500232620ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "bufio" "context" "fmt" "os" "strconv" "strings" "time" redis "github.com/go-redis/redis/v8" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Redis App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // normally, production code wouldn't require the WaitForConnection call, // but for an extremely short-lived script, we want to be sure we are // connected before we've already exited. app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("ping txn") opts := &redis.Options{ Addr: "localhost:6379", } client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context which includes // the transaction. // ctx := newrelic.NewContext(context.Background(), txn) pipe := client.WithContext(ctx).Pipeline() incr := pipe.Incr(ctx, "pipeline_counter") pipe.Expire(ctx, "pipeline_counter", time.Hour) _, err = pipe.Exec(ctx) fmt.Println(incr.Val(), err) result, err := client.Do(ctx, "INFO", "STATS").Result() if err != nil { panic(err) } hits := 0 misses := 0 if stats, ok := result.(string); ok { sc := bufio.NewScanner(strings.NewReader(stats)) for sc.Scan() { fields := strings.Split(sc.Text(), ":") if len(fields) == 2 { if v, err := strconv.Atoi(fields[1]); err == nil { switch fields[0] { case "keyspace_hits": hits = v case "keyspace_misses": misses = v } } } } } if hits+misses > 0 { app.RecordCustomMetric("Custom/RedisCache/HitRatio", float64(hits)/(float64(hits+misses))) } txn.End() app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nrredis-v8/go.mod000066400000000000000000000004161510742411500214470ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrredis-v8 // https://github.com/go-redis/redis/blob/master/go.mod go 1.24 require ( github.com/go-redis/redis/v8 v8.4.0 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrredis-v8/nrredis.go000066400000000000000000000053341510742411500223420ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrredis instruments github.com/go-redis/redis/v8. // // Use this package to instrument your go-redis/redis/v8 calls without having to // manually create DatastoreSegments. package nrredis import ( "context" "net" "strings" redis "github.com/go-redis/redis/v8" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "datastore", "redis") } type contextKeyType struct{} type hook struct { segment newrelic.DatastoreSegment } var ( segmentContextKey = contextKeyType(struct{}{}) ) // NewHook creates a redis.Hook to instrument Redis calls. Add it to your // client, then ensure that all calls contain a context which includes the // transaction. The options are optional. Provide them to get instance metrics // broken out by host and port. The hook returned can be used with // redis.Client, redis.ClusterClient, and redis.Ring. func NewHook(opts *redis.Options) redis.Hook { h := hook{} h.segment.Product = newrelic.DatastoreRedis if opts != nil { // Per https://godoc.org/github.com/go-redis/redis#Options the // network should either be tcp or unix, and the default is tcp. if opts.Network == "unix" { h.segment.Host = "localhost" h.segment.PortPathOrID = opts.Addr } else if host, port, err := net.SplitHostPort(opts.Addr); err == nil { if "" == host { host = "localhost" } h.segment.Host = host h.segment.PortPathOrID = port } } return h } func (h hook) before(ctx context.Context, operation string) (context.Context, error) { txn := newrelic.FromContext(ctx) if txn == nil { return ctx, nil } s := h.segment s.StartTime = txn.StartSegmentNow() s.Operation = operation ctx = context.WithValue(ctx, segmentContextKey, &s) return ctx, nil } func (h hook) after(ctx context.Context) { if segment, ok := ctx.Value(segmentContextKey).(interface{ End() }); ok { segment.End() } } func (h hook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { return h.before(ctx, cmd.Name()) } func (h hook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { h.after(ctx) return nil } func pipelineOperation(cmds []redis.Cmder) string { operations := make([]string, 0, len(cmds)) for _, cmd := range cmds { operations = append(operations, cmd.Name()) } return "pipeline:" + strings.Join(operations, ",") } func (h hook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { return h.before(ctx, pipelineOperation(cmds)) } func (h hook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { h.after(ctx) return nil } go-agent-3.42.0/v3/integrations/nrredis-v8/nrredis_example_test.go000066400000000000000000000025651510742411500251170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis_test import ( "context" "fmt" redis "github.com/go-redis/redis/v8" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func getTransaction() *newrelic.Transaction { return nil } func Example_client() { opts := &redis.Options{Addr: "localhost:6379"} client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.WithContext(ctx).Ping(ctx).Result() fmt.Println(pong, err) } func Example_clusterClient() { client := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, }) // // Step 1: Add a nrredis.NewHook() to your redis cluster client. // client.AddHook(nrredis.NewHook(nil)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.WithContext(ctx).Ping(ctx).Result() fmt.Println(pong, err) } go-agent-3.42.0/v3/integrations/nrredis-v8/nrredis_test.go000066400000000000000000000111751510742411500234010ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis import ( "context" "net" "testing" redis "github.com/go-redis/redis/v8" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) func emptyDialer(context.Context, string, string) (net.Conn, error) { return &net.TCPConn{}, nil } func TestPing(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(nil)) client.WithContext(ctx).Ping(ctx) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, }) } func TestPingWithOptionsAndAddress(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(opts)) client.WithContext(ctx).Ping(ctx) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/instance/Redis/myhost/myport", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, }) } func TestPipelineOperation(t *testing.T) { // As of Jan 16, 2020, it is impossible to test pipeline operations using // a &net.TCPConn{}, so we will have to make do with this. if op := pipelineOperation(nil); op != "pipeline:" { t.Error(op) } ctx := context.Background() cmds := []redis.Cmder{redis.NewCmd(ctx, "GET"), redis.NewCmd(ctx, "SET")} if op := pipelineOperation(cmds); op != "pipeline:get,set" { t.Error(op) } } func TestNewHookAddress(t *testing.T) { testcases := []struct { network string address string expHost string expPort string }{ // examples from net.Dial https://godoc.org/net#Dial { network: "tcp", address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "", // tcp is assumed if missing address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "tcp", address: "192.0.2.1:http", expHost: "192.0.2.1", expPort: "http", }, { network: "tcp", address: "198.51.100.1:80", expHost: "198.51.100.1", expPort: "80", }, { network: "tcp", address: ":80", expHost: "localhost", expPort: "80", }, { network: "tcp", address: "0.0.0.0:80", expHost: "0.0.0.0", expPort: "80", }, { network: "tcp", address: "[::]:80", expHost: "::", expPort: "80", }, { network: "unix", address: "path/to/socket", expHost: "localhost", expPort: "path/to/socket", }, } for _, tc := range testcases { t.Run(tc.network+","+tc.address, func(t *testing.T) { hk := NewHook(&redis.Options{ Network: tc.network, Addr: tc.address, }).(hook) if hk.segment.Host != tc.expHost { t.Errorf("incorrect host: expect=%s actual=%s", tc.expHost, hk.segment.Host) } if hk.segment.PortPathOrID != tc.expPort { t.Errorf("incorrect port: expect=%s actual=%s", tc.expPort, hk.segment.PortPathOrID) } }) } } go-agent-3.42.0/v3/integrations/nrredis-v9/000077500000000000000000000000001510742411500203415ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v9/LICENSE.txt000066400000000000000000000264501510742411500221730ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrredis-v9/README.md000066400000000000000000000007321510742411500216220ustar00rootroot00000000000000# v3/integrations/nrredis-v9 [![pkg.go.dev](https://pkg.go.dev/github.com/newrelic/go-agent/v3/integrations/nrredis-v9?status.svg)](https://pkg.go.dev/github.com/newrelic/go-agent/v3/integrations/nrredis-v9) Package `nrredis` instruments `"github.com/redis/go-redis/v9"`. ```go import nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v9" ``` For more information, see [pkg.go.dev](https://pkg.go.dev/github.com/newrelic/go-agent/v3/integrations/nrredis-v9). go-agent-3.42.0/v3/integrations/nrredis-v9/example/000077500000000000000000000000001510742411500217745ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v9/example/main.go000066400000000000000000000037541510742411500232600ustar00rootroot00000000000000// Copyright 2023 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "bufio" "context" "fmt" "os" "strconv" "strings" "time" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v9" newrelic "github.com/newrelic/go-agent/v3/newrelic" redis "github.com/redis/go-redis/v9" ) func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Redis App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } // normally, production code wouldn't require the WaitForConnection call, // but for an extremely short-lived script, we want to be sure we are // connected before we've already exited. app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("ping txn") opts := &redis.Options{ Addr: "localhost:6379", } client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context which includes // the transaction. // ctx := newrelic.NewContext(context.Background(), txn) pipe := client.Pipeline() incr := pipe.Incr(ctx, "pipeline_counter") pipe.Expire(ctx, "pipeline_counter", time.Hour) _, err = pipe.Exec(ctx) fmt.Println(incr.Val(), err) result, err := client.Do(ctx, "INFO", "STATS").Result() if err != nil { panic(err) } hits := 0 misses := 0 if stats, ok := result.(string); ok { sc := bufio.NewScanner(strings.NewReader(stats)) for sc.Scan() { fields := strings.Split(sc.Text(), ":") if len(fields) == 2 { if v, err := strconv.Atoi(fields[1]); err == nil { switch fields[0] { case "keyspace_hits": hits = v case "keyspace_misses": misses = v } } } } } if hits+misses > 0 { app.RecordCustomMetric("Custom/RedisCache/HitRatio", float64(hits)/(float64(hits+misses))) } txn.End() app.Shutdown(5 * time.Second) } go-agent-3.42.0/v3/integrations/nrredis-v9/go.mod000066400000000000000000000004631510742411500214520ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrredis-v9 // https://github.com/redis/go-redis/blob/a38f75b640398bd709ee46c778a23e80e09d48b5/go.mod#L3 go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/redis/go-redis/v9 v9.0.2 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrredis-v9/nrredis.go000066400000000000000000000150521510742411500223410ustar00rootroot00000000000000// Copyright 2023 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrredis instruments github.com/redis/go-redis/v9. // // Use this package to instrument your redis/go-redis/v9 calls without having to // manually create DatastoreSegments. package nrredis import ( "context" "fmt" "net" "slices" "strings" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" redis "github.com/redis/go-redis/v9" ) func init() { internal.TrackUsage("integration", "datastore", "redis") } type contextKeyType struct{} type hook struct { includeKeys bool operationSet []string agentConfiguration struct { retrieved bool queryParametersEnabled bool rawQueryEnabled bool } segment newrelic.DatastoreSegment } var _ redis.Hook = (*hook)(nil) var ( segmentContextKey = contextKeyType(struct{}{}) ) // NewHookWithOptions is like NewHook but allows integration-specific // options to be included as well, such as ConfigDatastoreKeysEanbled. func NewHookWithOptions(opts *redis.Options, o ...nrredisOpts) redis.Hook { h := hook{} for _, opt := range o { opt(&h) } return newHook(h, opts) } type nrredisOpts func(*hook) // ConfigDatastoreKeysEnabled controls whether we report the names of // keys along with the datastore operations in our telemetry. Since the // keys themselves might contain sensitive information in some databases // unlike, say, the general case of a parameterized SQL query with placeholders, // this is disabled by default. However, if you know your keys are safe to // expose in your telemetry data and wish to see them there, call this method // on your hook value with a true parameter. // // N.B. for Redis database operations, note that for our purposes what we are // referring to here as "keys" are in fact merely the 2nd parameter in the // operation parameter list being sent to the Redis server. Typically this // will be the key or similar ID for the operation at hand, but this will vary // based on the particular operation being performed. Take care to ensure that // it is acceptable to record this parameter in your telemetry dataset before // enabling this option, or restrict the operations for which you wish to expose // this data by also specifying the ConfigLimitOperations option. // // If the agent has also enabled raw database queries via the ConfigDatastoreRawQuery // option, then the full redis operation will be exposed instead of just the operation // and following parameter since that option enables the forwarding of the full database // command string and all data. func ConfigDatastoreKeysEnabled(enabled bool) func(*hook) { return func(h *hook) { h.includeKeys = enabled } } // ConfigLimitOperations restricts the set of operations which will report their // keys (assuming ConfigDatastoreKeysEnabled is also given with a true value) // to only those operations whose names match those passed to this option. func ConfigLimitOperations(name ...string) func(*hook) { return func(h *hook) { for _, n := range name { h.operationSet = append(h.operationSet, n) } } } // NewHook creates a redis.Hook to instrument Redis calls. Add it to your // client, then ensure that all calls contain a context which includes the // transaction. The options are optional. Provide them to get instance metrics // broken out by host and port. The hook returned can be used with // redis.Client, redis.ClusterClient, and redis.Ring. func NewHook(opts *redis.Options) redis.Hook { h := hook{} return newHook(h, opts) } func newHook(h hook, opts *redis.Options) redis.Hook { h.segment.Product = newrelic.DatastoreRedis if opts == nil { return h } // Per https://pkg.go.dev/github.com/redis/go-redis#Options the // network should either be tcp or unix, and the default is tcp. if opts.Network == "unix" { h.segment.Host = "localhost" h.segment.PortPathOrID = opts.Addr return h } if host, port, err := net.SplitHostPort(opts.Addr); err == nil { if host == "" { host = "localhost" } h.segment.Host = host h.segment.PortPathOrID = port } return h } func (h hook) before(ctx context.Context, operation string) context.Context { txn := newrelic.FromContext(ctx) if txn == nil { return ctx } s := h.segment s.StartTime = txn.StartSegmentNow() s.Operation = operation ctx = context.WithValue(ctx, segmentContextKey, &s) return ctx } func (h hook) after(ctx context.Context) { if segment, ok := ctx.Value(segmentContextKey).(interface{ End() }); ok { segment.End() } } func pipelineOperation(cmds []redis.Cmder) string { operations := make([]string, 0, len(cmds)) for _, cmd := range cmds { operations = append(operations, cmd.Name()) } return "pipeline:" + strings.Join(operations, ",") } func (h hook) DialHook(next redis.DialHook) redis.DialHook { return next // just continue the hook } func (h hook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { ctx = h.before(ctx, cmd.Name()) err := next(ctx, cmd) h.after(ctx) if ctx != nil && h.includeKeys { // Only go to the expense of collecting this data if we are going to // possibly be reporting it out later, so check the agent's configuration // (but only once) if !h.agentConfiguration.retrieved { if txn := newrelic.FromContext(ctx); txn != nil { if cfg, isValid := txn.Application().Config(); isValid { h.agentConfiguration.retrieved = true h.agentConfiguration.queryParametersEnabled = cfg.DatastoreTracer.QueryParameters.Enabled h.agentConfiguration.rawQueryEnabled = cfg.DatastoreTracer.RawQuery.Enabled } } } operationName := cmd.Name() if len(h.operationSet) == 0 || slices.ContainsFunc(h.operationSet, func(op string) bool { return strings.EqualFold(operationName, op) }) { args := cmd.Args() if args != nil && len(args) > 0 { if h.agentConfiguration.rawQueryEnabled { h.segment.RawQuery = fmt.Sprintf("%v", args) } if len(args) > 1 { if h.agentConfiguration.queryParametersEnabled { if h.segment.QueryParameters == nil { h.segment.QueryParameters = make(map[string]any) } h.segment.QueryParameters["key"] = args[1] h.segment.ParameterizedQuery = fmt.Sprintf("%v %v", args[0], args[1]) } } } } } return err } } func (h hook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { ctx = h.before(ctx, pipelineOperation(cmds)) err := next(ctx, cmds) h.after(ctx) return err } } go-agent-3.42.0/v3/integrations/nrredis-v9/nrredis_db_test.go000066400000000000000000000063721510742411500240520ustar00rootroot00000000000000//go:build local_redis_test // +build local_redis_test // Copyright 2023 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis import ( "context" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" redis "github.com/redis/go-redis/v9" ) // Performs live database testing with an instance of a local Redis database // on port 6379. func TestRealDatabaseOperations(t *testing.T) { db := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) app := integrationsupport.NewTestApp(nil, nil) // txn := app.StartTransaction("build") ctx := context.Background() // ctx := newrelic.NewContext(context.Background(), txn) db.AddHook(NewHookWithOptions(nil, ConfigDatastoreKeysEnabled(true), ConfigLimitOperations("get", "set"), )) size, err := db.DBSize(ctx).Result() if err != nil { t.Fatalf("unable to get db size: %v", err) } if size != 0 { t.Fatalf("database is not empty (size=%v), refusing to overwrite existing data; empty database before running tests", size) } testData := []struct { Key string Value any }{ {"Foo", "Bar"}, {"Spam", "Eggs"}, {"answer", 42}, {"maybe", true}, } for i, d := range testData { if err := db.Set(ctx, d.Key, d.Value, 0).Err(); err != nil { t.Fatalf("database store of item %d failed: %v", i, err) } } // txn.End() txn2 := app.StartTransaction("query") ctx2 := newrelic.NewContext(context.Background(), txn2) for i, d := range testData { r := db.Get(ctx2, d.Key) v, err := r.Result() if err != nil { t.Fatalf("retrieval of item %d failed: %v", i, err) } switch d.Value.(type) { case int: ri, err := r.Int() if err != nil { t.Errorf("retrieved value \"%s\" of item %d isn't an integer", v, i) } if ri != d.Value { t.Errorf("retrieved value of item %d was %v, expected %v", i, ri, d.Value) } case bool: rb, err := r.Bool() if err != nil { t.Errorf("retrieved value \"%s\" of item %d isn't a boolean", v, i) } if rb != d.Value { t.Errorf("retrieved value of item %d was %v, expected %v", i, rb, d.Value) } default: if v != d.Value { t.Errorf("retrieved value of item %d was %v, expected %v", i, v, d.Value) } } } txn2.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/query", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/query", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/operation/Redis/get", Forced: nil}, {Name: "Datastore/operation/Redis/get", Scope: "OtherTransaction/Go/query", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, }) // app.ExpectSpanEvents(t, []internal.WantEvent{}) app.ExpectSlowQueries(t, []internal.WantSlowQuery{}) // c.DatastoreTracer.SlowQuery.Threshold = 10 * time.Millisecond // c.DatastoreTracer.RawQuery.Enabled = true } go-agent-3.42.0/v3/integrations/nrredis-v9/nrredis_example_test.go000066400000000000000000000025231510742411500251120ustar00rootroot00000000000000// Copyright 2023 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis_test import ( "context" "fmt" nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v9" newrelic "github.com/newrelic/go-agent/v3/newrelic" redis "github.com/redis/go-redis/v9" ) func getTransaction() *newrelic.Transaction { return nil } func Example_client() { opts := &redis.Options{Addr: "localhost:6379"} client := redis.NewClient(opts) // // Step 1: Add a nrredis.NewHook() to your redis client. // client.AddHook(nrredis.NewHook(opts)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.Ping(ctx).Result() fmt.Println(pong, err) } func Example_clusterClient() { client := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, }) // // Step 1: Add a nrredis.NewHook() to your redis cluster client. // client.AddHook(nrredis.NewHook(nil)) // // Step 2: Ensure that all client calls contain a context with includes // the transaction. // txn := getTransaction() ctx := newrelic.NewContext(context.Background(), txn) pong, err := client.Ping(ctx).Result() fmt.Println(pong, err) } go-agent-3.42.0/v3/integrations/nrredis-v9/nrredis_test.go000066400000000000000000000126611510742411500234030ustar00rootroot00000000000000// Copyright 2023 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrredis import ( "context" "net" "testing" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" redis "github.com/redis/go-redis/v9" ) func emptyDialer(context.Context, string, string) (net.Conn, error) { return &net.TCPConn{}, nil } func TestPing(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(nil)) client.Ping(ctx) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, }) } func TestPingWithOptionsAndAddress(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(opts)) client.Ping(ctx) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/instance/Redis/myhost/myport", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Forced: nil}, {Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, }) } func TestPingAndHelloWithPipeline(t *testing.T) { opts := &redis.Options{ Dialer: emptyDialer, Addr: "myhost:myport", } client := redis.NewClient(opts) app := integrationsupport.NewTestApp(nil, nil) txn := app.StartTransaction("txnName") ctx := newrelic.NewContext(context.Background(), txn) client.AddHook(NewHook(opts)) p := client.Pipeline() p.Ping(ctx) p.Hello(ctx, 3, "", "", "") p.Exec(ctx) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil}, {Name: "OtherTransaction/all", Forced: nil}, {Name: "OtherTransactionTotalTime", Forced: nil}, {Name: "Datastore/all", Forced: nil}, {Name: "Datastore/allOther", Forced: nil}, {Name: "Datastore/Redis/all", Forced: nil}, {Name: "Datastore/Redis/allOther", Forced: nil}, {Name: "Datastore/instance/Redis/myhost/myport", Forced: nil}, {Name: "Datastore/operation/Redis/pipeline:ping,hello", Forced: nil}, {Name: "Datastore/operation/Redis/pipeline:ping,hello", Scope: "OtherTransaction/Go/txnName", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Forced: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Forced: nil}, }) } func TestNewHookAddress(t *testing.T) { testcases := []struct { network string address string expHost string expPort string }{ // examples from net.Dial https://pkg.go.dev/net#Dial { network: "tcp", address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "", // tcp is assumed if missing address: "golang.org:http", expHost: "golang.org", expPort: "http", }, { network: "tcp", address: "192.0.2.1:http", expHost: "192.0.2.1", expPort: "http", }, { network: "tcp", address: "198.51.100.1:80", expHost: "198.51.100.1", expPort: "80", }, { network: "tcp", address: ":80", expHost: "localhost", expPort: "80", }, { network: "tcp", address: "0.0.0.0:80", expHost: "0.0.0.0", expPort: "80", }, { network: "tcp", address: "[::]:80", expHost: "::", expPort: "80", }, { network: "unix", address: "path/to/socket", expHost: "localhost", expPort: "path/to/socket", }, } for _, tc := range testcases { t.Run(tc.network+","+tc.address, func(t *testing.T) { hk := NewHook(&redis.Options{ Network: tc.network, Addr: tc.address, }).(hook) if hk.segment.Host != tc.expHost { t.Errorf("incorrect host: expect=%s actual=%s", tc.expHost, hk.segment.Host) } if hk.segment.PortPathOrID != tc.expPort { t.Errorf("incorrect port: expect=%s actual=%s", tc.expPort, hk.segment.PortPathOrID) } }) } } go-agent-3.42.0/v3/integrations/nrredis-v9/util/000077500000000000000000000000001510742411500213165ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrredis-v9/util/empty_db.go000066400000000000000000000024161510742411500234530ustar00rootroot00000000000000package main import ( "bufio" "context" "fmt" "log" "os" "strings" "github.com/redis/go-redis/v9" ) func main() { db := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) ctx := context.Background() size, err := db.DBSize(ctx).Result() if err != nil { log.Fatalf("unable to determine database size: %v", err) } if size == 0 { log.Fatalf("database is already empty; Nothing to do here") } fmt.Printf("database size: %v. Empty database? [y/N] ", size) reader := bufio.NewReader(os.Stdin) input, _ := reader.ReadString('\n') if strings.ToLower(strings.TrimSpace(input)) != "y" { log.Fatalf("abandoning operation at user request") } // we'll retrieve and remove the contents individually so we also show what they were // since we also use this as a debugging tool of sorts for the data in the database. keys, err := db.Keys(ctx, "*").Result() if err != nil { log.Fatalf("unable to retrieve the keys: %v", err) } for i, key := range keys { v, err := db.Get(ctx, key).Result() if err != nil { log.Fatalf("unable to retrieve value #%d (key \"%s\"): %v", i, key, err) } fmt.Printf("#%d: %s: %s\n", i, key, v) if err = db.Del(ctx, key).Err(); err != nil { log.Fatalf("unable to delete value #%d (key \"%s\"): %v", i, key, err) } } } go-agent-3.42.0/v3/integrations/nrsarama/000077500000000000000000000000001510742411500201435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsarama/LICENSE.txt000066400000000000000000000264501510742411500217750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrsarama/consumer.go000066400000000000000000000110561510742411500223300ustar00rootroot00000000000000package nrsarama import ( "context" "net/http" "github.com/Shopify/sarama" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "messagebroker", "saramakafka") } type ConsumerWrapper struct { consumerGroup sarama.ConsumerGroup } type ConsumerHandler struct { app *newrelic.Application txn *newrelic.Transaction topic string clientID string saramaConfig *sarama.Config messageHandler func(ctx context.Context, message *sarama.ConsumerMessage) } // NOTE: Creates and ends one transaction per claim consumed // NewConsumerHandlerFromApp takes in a new relic application and creates a transaction using it func NewConsumerHandlerFromApp(app *newrelic.Application, topic string, clientID string, saramaConfig *sarama.Config, messageHandler func(ctx context.Context, message *sarama.ConsumerMessage)) *ConsumerHandler { return &ConsumerHandler{ app: app, topic: topic, messageHandler: messageHandler, saramaConfig: saramaConfig, clientID: clientID, } } // NewConsumerHandlerFromTxn takes in a new relic transaction. No application instance is required func NewConsumerHandlerFromTxn(txn *newrelic.Transaction, topic string, clientID string, saramaConfig *sarama.Config, messageHandler func(ctx context.Context, message *sarama.ConsumerMessage)) *ConsumerHandler { return &ConsumerHandler{ txn: txn, topic: topic, messageHandler: messageHandler, saramaConfig: saramaConfig, clientID: clientID, } } func (cw *ConsumerWrapper) Consume(ctx context.Context, handler *ConsumerHandler) error { txn := newrelic.FromContext(ctx) consume := cw.consumerGroup.Consume(ctx, []string{handler.topic}, handler) if consume != nil { txn.Application().RecordCustomMetric("MessageBroker/Kafka/Heartbeat/Fail", 1.0) } return nil } // Setup is ran at the beginning of a new session func (ch *ConsumerHandler) Setup(_ sarama.ConsumerGroupSession) error { // Record session timeout/poll timeout intervals ch.app.RecordCustomMetric("MessageBroker/Kafka/Heartbeat/SessionTimeout", ch.saramaConfig.Consumer.Group.Session.Timeout.Seconds()) ch.app.RecordCustomMetric("MessageBroker/Kafka/Heartbeat/PollTimeout", ch.saramaConfig.Consumer.Group.Heartbeat.Interval.Seconds()) return nil } // Cleanup is ran at the end of a new session func (ch *ConsumerHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil } func ClaimIngestion(ch *ConsumerHandler, session sarama.ConsumerGroupSession, message *sarama.ConsumerMessage) { // if txn exists, make claims segments of that txn otherwise create a new one txn := ch.txn if ch.txn == nil { txn = ch.app.StartTransaction("kafkaconsumer") } ctx := newrelic.NewContext(context.Background(), txn) segment := txn.StartSegment("Message/Kafka/Topic/Consume/Named/" + ch.topic) // Deserialized key/value deserializeKeySegment := txn.StartSegment("MessageBroker/Kafka/Topic/Named/" + ch.topic + "/Deserialization/Key") key := string(message.Key) deserializeKeySegment.End() deserializeVaueSegment := txn.StartSegment("MessageBroker/Kafka/Topic/Named/" + ch.topic + "/Deserialization/Value") value := string(message.Value) deserializeVaueSegment.End() ch.processMessage(ctx, message, key, value) segment.End() session.MarkMessage(message, "") // Heartbeat metric to log a new message received successfully txn.Application().RecordCustomMetric("MessageBroker/Kafka/Heartbeat/Receive", 1.0) txn.End() } func (ch *ConsumerHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { for message := range claim.Messages() { ClaimIngestion(ch, session, message) } return nil } func (ch *ConsumerHandler) processMessage(ctx context.Context, message *sarama.ConsumerMessage, key string, value string) { txn := newrelic.FromContext(ctx) messageHandlingSegment := txn.StartSegment("Message/Kafka/Topic/Consume/Named/" + ch.topic + "/MessageProcessing/") ch.messageHandler(ctx, message) byteCount := float64(len(message.Value)) hdrs := http.Header{} for _, hdr := range message.Headers { hdrs.Add(string(hdr.Key), string(hdr.Value)) } txn.InsertDistributedTraceHeaders(hdrs) txn.AddAttribute("kafka.consume.byteCount", byteCount) txn.AddAttribute("kafka.consume.ClientID", ch.clientID) txn.Application().RecordCustomMetric("Message/Kafka/Topic/Named/"+ch.topic+"/Received/Bytes", byteCount) txn.Application().RecordCustomMetric("Message/Kafka/Topic/Named/"+ch.topic+"/Received/Messages", 1) messageHandlingSegment.End() } go-agent-3.42.0/v3/integrations/nrsarama/example/000077500000000000000000000000001510742411500215765ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsarama/example/consumer/000077500000000000000000000000001510742411500234315ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsarama/example/consumer/consumerexample.go000066400000000000000000000041631510742411500271730ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "time" "github.com/Shopify/sarama" nrsaramaconsumer "github.com/newrelic/go-agent/v3/integrations/nrsarama" "github.com/newrelic/go-agent/v3/newrelic" ) var brokers = []string{"localhost:9092"} // Custom message handler that controls what happens when a new message is received by the consumer // Note: delay is present only to simulate handling of message func messageHandler(ctx context.Context, msg *sarama.ConsumerMessage) { log.Printf("received message %v\n", string(msg.Key)) delay := time.Duration(2 * time.Millisecond) time.Sleep(delay) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Kafka App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigDistributedTracerEnabled(true), ) if nil != err { fmt.Println(err) os.Exit(1) } // Wait for the application to connect. if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } // Setup sarama config, including session timeout/heartbeat intervals config := sarama.NewConfig() config.ClientID = "CustomClientID" config.Consumer.Group.Session.Timeout = 10 * time.Second config.Consumer.Group.Heartbeat.Interval = 3 * time.Second // Create new sarama consumer group consumerGroup, err := sarama.NewConsumerGroup(brokers, "test-group", config) if nil != err { fmt.Println(err) } kafkaTopicName := "topicName" // Create new kafka consumer handler (using an application instance) // Alternatively, you can create a transaction, use the function NewConsumerHandlerFromTxn, and not have to pass in an app instance at all // If an application instance is passed in, the default transaction name will be "kafkaconsumer" handler := nrsaramaconsumer.NewConsumerHandlerFromApp(app, kafkaTopicName, config.ClientID, config, messageHandler) for { err := consumerGroup.Consume(context.Background(), []string{kafkaTopicName}, handler) if nil != err { fmt.Println(err) } } // NOTE: Whenever the consumer no longer accepts messages be sure to close it out using consumerGroup.Close() } go-agent-3.42.0/v3/integrations/nrsarama/example/producer/000077500000000000000000000000001510742411500234215ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsarama/example/producer/producerexample.go000066400000000000000000000027261510742411500271560ustar00rootroot00000000000000package main import ( "fmt" "os" "strconv" "time" "github.com/Shopify/sarama" nrsaramaproducer "github.com/newrelic/go-agent/v3/integrations/nrsarama" "github.com/newrelic/go-agent/v3/newrelic" ) var brokers = []string{"localhost:9092"} func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Kafka App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDistributedTracerEnabled(true), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Wait for the application to connect. if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } // Sarama Producer configuration settings config := sarama.NewConfig() config.Producer.Partitioner = sarama.NewRandomPartitioner config.Producer.RequiredAcks = sarama.WaitForLocal config.Producer.Return.Successes = true // Create Producer producer, err := sarama.NewSyncProducer(brokers, config) if nil != err { fmt.Println(err) } // Start new transaction txn := app.StartTransaction("kafkaproducer") kw := nrsaramaproducer.NewProducerWrapper(producer, txn) topic := "topicName" // Generate and send multiple messages numMessages := 10 for i := 0; i < numMessages; i++ { key := []byte("key-" + strconv.Itoa(i)) msg := []byte("test Message " + strconv.Itoa(i)) err = kw.SendMessage(topic, key, msg) if nil != err { fmt.Println(err) } } txn.End() app.Shutdown(10 * time.Second) } go-agent-3.42.0/v3/integrations/nrsarama/go.mod000066400000000000000000000004121510742411500212460ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrsarama go 1.24 toolchain go1.24.2 require ( github.com/Shopify/sarama v1.38.1 github.com/newrelic/go-agent/v3 v3.42.0 github.com/stretchr/testify v1.8.1 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrsarama/nrsarama_test.go000066400000000000000000000153521510742411500233430ustar00rootroot00000000000000package nrsarama import ( "context" "log" "net/http" "reflect" "testing" "time" "github.com/Shopify/sarama" "github.com/Shopify/sarama/mocks" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "github.com/stretchr/testify/mock" ) type MockConsumerGroupSession struct { mock.Mock } func (m *MockConsumerGroupSession) MarkMessage(msg *sarama.ConsumerMessage, metadata string) {} func (m *MockConsumerGroupSession) Commit() {} func (m *MockConsumerGroupSession) MarkOffset(topic string, partition int32, offset int64, metadata string) { } func (m *MockConsumerGroupSession) ResetOffset(topic string, partition int32, offset int64, metadata string) { } func (m *MockConsumerGroupSession) Context() context.Context { return nil } func (m *MockConsumerGroupSession) Claims() map[string][]int32 { return nil } func (m *MockConsumerGroupSession) MemberID() string { return "" } func (m *MockConsumerGroupSession) GenerationID() int32 { return 0 } func TestProducerSendMessage(t *testing.T) { producer := mocks.NewSyncProducer(t, nil) producer.ExpectSendMessageAndSucceed() txn := &newrelic.Transaction{} kw := NewProducerWrapper(producer, txn) // Compose message key := []byte("key") msg := []byte("value") err := kw.SendMessage("topicName", key, msg) if nil != err { t.Error(err) } } func TestProducerSetHeaders(t *testing.T) { producer := mocks.NewSyncProducer(t, nil) txn := &newrelic.Transaction{} kw := NewProducerWrapper(producer, txn) // Create kafka message keyEncoded := sarama.ByteEncoder("key") valEncoded := sarama.ByteEncoder("val") msg := &sarama.ProducerMessage{ Topic: "topic", Key: keyEncoded, Value: valEncoded, } // Set Headers carrier := kw.carrier(msg) carrier.Set("k", "v") // check to see if headers set in carrier are correct carrierhdrs := carrier.Header hdrs := make(http.Header) hdrs.Set("k", "v") eq := reflect.DeepEqual(carrierhdrs, hdrs) if !eq { t.Error("actual headers does not match what is expected", carrierhdrs, hdrs) } } // Custom message handler that controls what happens when a new message is received by the consumer func messageHandler(ctx context.Context, msg *sarama.ConsumerMessage) { log.Printf("received message %v\n", string(msg.Key)) } func TestConsumerHandlerFromApp(t *testing.T) { app := integrationsupport.NewBasicTestApp() // Setup sarama config, including session timeout/heartbeat intervals config := sarama.NewConfig() config.ClientID = "CustomClientID" config.Consumer.Group.Session.Timeout = 10 * time.Second config.Consumer.Group.Heartbeat.Interval = 3 * time.Second kafkaTopicName := "topicName" keyEncoded := sarama.ByteEncoder("key") encodedValue := sarama.ByteEncoder("value") msg := &sarama.ConsumerMessage{ Topic: "topic", Key: keyEncoded, Value: encodedValue, Headers: []*sarama.RecordHeader{}, } mockSession := new(MockConsumerGroupSession) ch := NewConsumerHandlerFromApp(app.Application, kafkaTopicName, config.ClientID, config, messageHandler) ClaimIngestion(ch, mockSession, msg) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/kafkaconsumer"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Key", Scope: "OtherTransaction/Go/kafkaconsumer", Forced: false, Data: nil}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName/MessageProcessing/", Scope: "OtherTransaction/Go/kafkaconsumer"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Value", Scope: "OtherTransaction/Go/kafkaconsumer", Forced: false, Data: nil}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName", Scope: "OtherTransaction/Go/kafkaconsumer"}, {Name: "OtherTransaction/all"}, {Name: "Custom/MessageBroker/Kafka/Heartbeat/Receive"}, {Name: "OtherTransaction/Go/kafkaconsumer"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Key"}, {Name: "Custom/Message/Kafka/Topic/Named/topicName/Received/Bytes"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Value"}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName/MessageProcessing/"}, {Name: "Custom/Message/Kafka/Topic/Named/topicName/Received/Messages"}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName"}, {Name: "OtherTransactionTotalTime"}, }) } func TestConsumerHandlerFromTxn(t *testing.T) { app := integrationsupport.NewBasicTestApp() // Setup sarama config, including session timeout/heartbeat intervals config := sarama.NewConfig() config.ClientID = "CustomClientID" config.Consumer.Group.Session.Timeout = 10 * time.Second config.Consumer.Group.Heartbeat.Interval = 3 * time.Second kafkaTopicName := "topicName" keyEncoded := sarama.ByteEncoder("key") encodedValue := sarama.ByteEncoder("value") msg := &sarama.ConsumerMessage{ Topic: "topic", Key: keyEncoded, Value: encodedValue, Headers: []*sarama.RecordHeader{}, } mockSession := new(MockConsumerGroupSession) txn := app.StartTransaction("kafkaconsumer") ch := NewConsumerHandlerFromTxn(txn, kafkaTopicName, config.ClientID, config, messageHandler) ClaimIngestion(ch, mockSession, msg) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransactionTotalTime/Go/kafkaconsumer"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Key", Scope: "OtherTransaction/Go/kafkaconsumer", Forced: false, Data: nil}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName/MessageProcessing/", Scope: "OtherTransaction/Go/kafkaconsumer"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Value", Scope: "OtherTransaction/Go/kafkaconsumer", Forced: false, Data: nil}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName", Scope: "OtherTransaction/Go/kafkaconsumer"}, {Name: "OtherTransaction/all"}, {Name: "Custom/MessageBroker/Kafka/Heartbeat/Receive"}, {Name: "OtherTransaction/Go/kafkaconsumer"}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Key"}, {Name: "Custom/Message/Kafka/Topic/Named/topicName/Received/Bytes"}, {Name: "Custom/MessageBroker/Kafka/Topic/Named/topicName/Deserialization/Value"}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName/MessageProcessing/"}, {Name: "Custom/Message/Kafka/Topic/Named/topicName/Received/Messages"}, {Name: "Custom/Message/Kafka/Topic/Consume/Named/topicName"}, {Name: "OtherTransactionTotalTime"}, }) } go-agent-3.42.0/v3/integrations/nrsarama/producer.go000066400000000000000000000037011510742411500223160ustar00rootroot00000000000000package nrsarama import ( "log" "net/http" "github.com/Shopify/sarama" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "messagebroker", "saramakafka") } type ProducerWrapper struct { producer sarama.SyncProducer txn *newrelic.Transaction } type KafkaMessageCarrier struct { http.Header msg *sarama.ProducerMessage } func NewProducerWrapper(producer sarama.SyncProducer, txn *newrelic.Transaction) *ProducerWrapper { return &ProducerWrapper{ producer: producer, txn: txn, } } func (pw *ProducerWrapper) carrier(msg *sarama.ProducerMessage) *KafkaMessageCarrier { return &KafkaMessageCarrier{ Header: make(http.Header), msg: msg, } } func (carrier KafkaMessageCarrier) Set(key, val string) { carrier.Header.Set(key, val) carrier.msg.Headers = append(carrier.msg.Headers, sarama.RecordHeader{ Key: []byte(key), Value: []byte(val), }) } func (pw *ProducerWrapper) SendMessage(topic string, key []byte, value []byte) error { // Traces for encoding key/value keyEncoding := pw.txn.StartSegment("MessageBroker/Kafka/Topic/Named/" + topic + "/Serialization/Key") keyEncoded := sarama.ByteEncoder(key) keyEncoding.End() valueEncoding := pw.txn.StartSegment("MessageBroker/Kafka/Topic/Named/" + topic + "/Serialization/Value") encodedValue := sarama.ByteEncoder(value) valueEncoding.End() // Create kafka message msg := &sarama.ProducerMessage{ Topic: topic, Key: keyEncoded, Value: encodedValue, } // DT Headers carrier := pw.carrier(msg) pw.txn.InsertDistributedTraceHeaders(carrier.Header) // Send message using kafka producer producerSegment := pw.txn.StartSegment("MessageBroker/Kafka/Topic/Produce/Named/" + topic) partition, offset, err := pw.producer.SendMessage(msg) defer producerSegment.End() if err != nil { return err } log.Printf("Sent to partion %v and the offset is %v", partition, offset) return nil } go-agent-3.42.0/v3/integrations/nrsecureagent/000077500000000000000000000000001510742411500212045ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsecureagent/README.md000066400000000000000000000005171510742411500224660ustar00rootroot00000000000000# Renamed In some early pre-release builds, there was an integration called `nrsecureagent`. This has now been renamed to `nrsecurityagent`. In case any documentation directed you to the old name, please use the new name instead and report an issue to correct the outdated reference in the documentation which still has the old name. go-agent-3.42.0/v3/integrations/nrsecurityagent/000077500000000000000000000000001510742411500215655ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsecurityagent/LICENSE.txt000066400000000000000000000264501510742411500234170ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrsecurityagent/README.md000066400000000000000000000154411510742411500230510ustar00rootroot00000000000000# v3/integrations/nrsecurityagent [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsecurityagent?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsecurityagent) The New Relic security agent analyzes your application for potentially exploitable vulnerabilities. **DO NOT** use this integration in your production environment. It is intended only for use in your development and testing phases. Since it will attempt to actually find and exploit vulnerabilities in your code, it may cause data loss or crash the application. Therefore it should only be used with test data in a non-production environment that does not connect to any production services. ## Learn More About IAST To learn how to use IAST with the New Relic Go Agent, [check out our documentation](https://docs.newrelic.com/docs/iast/use-iast/). ## Setup Instructions * Add this integration to your application by importing ``` import "github.com/newrelic/go-agent/v3/integrations/nrsecurityagent" ``` * Then, add code to initialize the integration after your call to `newrelic.NewApplication`: ``` app, err := newrelic.NewApplication( ... ) err := nrsecurityagent.InitSecurityAgent(app, nrsecurityagent.ConfigSecurityMode("IAST"), nrsecurityagent.ConfigSecurityValidatorServiceEndPointUrl("wss://csec.nr-data.net"), nrsecurityagent.ConfigSecurityEnable(true), ) ``` You can also configure the `nrsecurityagent` integration using a YAML-formatted configuration file: ``` err := nrsecurityagent.InitSecurityAgent(app, nrsecurityagent.ConfigSecurityFromYaml(), ) ``` In this case, you need to put the path to your YAML file in an environment variable: ``` NEW_RELIC_SECURITY_CONFIG_PATH={YOUR_PATH}/myappsecurity.yaml ``` The YAML file should have these contents (adjust as needed for your application): ``` # Determines whether the security data is sent to New Relic or not. When this is disabled and agent.enabled is # true, the security module will run but data will not be sent. Default is false. enabled: true # New Relic Security provides two modes: IAST and RASP # Default is IAST. Due to the invasive nature of IAST scanning, DO NOT enable this mode in either a # production environment or an environment where production data is processed. mode: IAST # New Relic Security's SaaS connection URL validator_service_url: wss://csec.nr-data.net # These are the category of security events that can be detected. Set to false to disable detection of # individual event types. Default is true for each event type. # This config is deprecated, detection: rci: enabled: true rxss: enabled: true deserialization: enabled: true # Unique test identifier when runnning IAST with CI/CD iast_test_identifier: "" # IAST scan controllers to get more control over IAST analysis scan_controllers: # maximum number of replay requests IAST Agent # can fire in a minute. Default is 3600. Minimum is 12 and maximum is 3600 iast_scan_request_rate_limit: 3600 # The number of application instances for a specific entity where IAST analysis is performed. # Values are 0 or 1, 0 signifies run on all application instances scan_instance_count: 0 # The scan_schedule configuration allows to specify when IAST scans should be executed scan_schedule: # The delay field specifies the delay in minutes before the IAST scan starts. # This allows to schedule the scan to start at a later time. In minutes, default is 0 min delay: 0 # The duration field specifies the duration of the IAST scan in minutes. # This determines how long the scan will run. In minutes, default is forever duration: 0 # The schedule field specifies a cron expression that defines when the IAST scan should start. schedule: "" # Allow continuously sample collection of IAST events regardless of scan schedule. Default is false always_sample_traces: false # The exclude_from_iast_scan configuration allows to specify APIs, parameters, # and categories that should not be scanned by Security Agents. exclude_from_iast_scan: # The api field specifies list of APIs using regular expression (regex) patterns that follow the syntax of Perl 5. # The regex pattern should provide a complete match for the URL without the endpoint. api: [] # The http_request_parameters configuration allows users to specify headers, query parameters, # and body keys that should be excluded from IAST scans. http_request_parameters: # A list of HTTP header keys. If a request includes any headers with these keys, # the corresponding IAST scan will be skipped. header: [] # A list of query parameter keys. The presence of these parameters in the request's query string # will lead to skipping the IAST scan. query: [] # A list of keys within the request body. If these keys are found in the body content, # the IAST scan will be omitted. body: [] # The iast_detection_category configuration allows to specify which categories # of vulnerabilities should not be detected by Security Agents. iast_detection_category: insecure_settings: false invalid_file_access: false sql_injection: false nosql_injection: false ldap_injection: false javascript_injection: false command_injection: false xpath_injection: false ssrf: false rxss: false ``` * Based on additional packages imported by the user application, add suitable instrumentation package imports. For more information, see https://github.com/newrelic/csec-go-agent#instrumentation-packages **Note**: To completely disable security, set `NEW_RELIC_SECURITY_AGENT_ENABLED` env to false. (Otherwise, there are some security hooks that will already be in place before any of the other configuration settings can be taken into account. This environment variable setting will prevent that from happening.) ## Instrument security-sensitive areas in your application If you are using the `nrgin`, `nrgrpc`, `nrmicro`, and/or `nrmongo` integrations, they now contain code to support security analysis of the data they handle. Additionally, the agent will inject vulnerability scanning to instrumented functions wherever possible, including datastore segments, SQL operations, and transactions. If you are opening an HTTP protocol endpoint, place the `newrelic.WrapListen` function around the endpoint name to enable vulnerability scanning against that endpoint. For example, ``` http.ListenAndServe(newrelic.WrapListen(":8000"), nil) ``` ## Start your application in your test environment Generate traffic against your application for the IAST agent to detect vulnerabilities. Once vulnerabilities are detected they will be reported in the vulnerabilities list. For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsecurityagent). go-agent-3.42.0/v3/integrations/nrsecurityagent/example/000077500000000000000000000000001510742411500232205ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsecurityagent/example/example.go000066400000000000000000000052661510742411500252130ustar00rootroot00000000000000// Copyright 2022 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "database/sql" "fmt" "io" "net/http" "os" "sync" "github.com/newrelic/go-agent/v3/integrations/nrsecurityagent" _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3" "github.com/newrelic/go-agent/v3/newrelic" ) func index(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello world") } func mysql(w http.ResponseWriter, r *http.Request) { var user_id = r.URL.Query().Get("input") var db *sql.DB db, err := sql.Open("nrsqlite3", "./csectest.db") defer db.Close() if err != nil { fmt.Println(err) w.Write([]byte("

Unable to Connect DATABASE

")) } statement, err := db.Prepare("CREATE TABLE IF NOT EXISTS USER (id INTEGER, name TEXT)") statement.Exec() defer statement.Close() txn := newrelic.FromContext(r.Context()) ctx := newrelic.NewContext(context.Background(), txn) res := db.QueryRowContext(ctx, "SELECT * FROM USER WHERE name = '"+user_id+"'") if err != nil { fmt.Println(err) w.Write([]byte("

ERROR

")) } else { fmt.Println(res) w.Write([]byte("Executed Query : SELECT * FROM USER WHERE name = '" + user_id + "'")) } } func async(w http.ResponseWriter, r *http.Request) { var filename = r.URL.Query().Get("input") txn := newrelic.FromContext(r.Context()) wg := &sync.WaitGroup{} wg.Add(1) go func(txn *newrelic.Transaction) { defer wg.Done() defer txn.StartSegment("async").End() os.Open(filename) }(txn.NewGoroutine()) segment := txn.StartSegment("wg.Wait") wg.Wait() segment.End() w.Write([]byte("done!")) } func rxss(w http.ResponseWriter, r *http.Request) { var input = r.URL.Query().Get("input") io.WriteString(w, input) } func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigFromEnvironment(), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigAppLogForwardingEnabled(true), newrelic.ConfigCodeLevelMetricsEnabled(true), newrelic.ConfigCodeLevelMetricsPathPrefix("go-agent/v3"), ) if err != nil { fmt.Println(err) os.Exit(1) } err = nrsecurityagent.InitSecurityAgent( app, nrsecurityagent.ConfigSecurityMode("IAST"), nrsecurityagent.ConfigSecurityValidatorServiceEndPointUrl("wss://csec.nr-data.net"), nrsecurityagent.ConfigSecurityEnable(true), ) if err != nil { fmt.Println(err) os.Exit(1) } http.HandleFunc(newrelic.WrapHandleFunc(app, "/", index)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/mysql", mysql)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/rxss", rxss)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/async", async)) http.ListenAndServe(newrelic.WrapListen(":8000"), nil) } go-agent-3.42.0/v3/integrations/nrsecurityagent/go.mod000066400000000000000000000004701510742411500226740ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent go 1.24 require ( github.com/newrelic/csec-go-agent v1.6.0 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0 gopkg.in/yaml.v2 v2.4.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrsecurityagent/nrsecurityagent.go000066400000000000000000000342771510742411500253570ustar00rootroot00000000000000// Copyright 2022 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrsecurityagent import ( "fmt" "io/ioutil" "os" "strconv" "strings" securityAgent "github.com/newrelic/csec-go-agent" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "gopkg.in/yaml.v2" ) func init() { internal.TrackUsage("integration", "securityagent") } type SecurityConfig struct { securityAgent.SecurityAgentConfig Error error } // defaultSecurityConfig creates a SecurityConfig value populated with default settings. func defaultSecurityConfig() SecurityConfig { cfg := SecurityConfig{} cfg.Security.Enabled = false cfg.Security.Validator_service_url = "wss://csec.nr-data.net" cfg.Security.Mode = "IAST" cfg.Security.Agent.Enabled = true cfg.Security.Detection.Rxss.Enabled = true cfg.Security.Request.BodyLimit = 300 cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Header = make([]string, 0) cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Body = make([]string, 0) cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Query = make([]string, 0) cfg.Security.ExcludeFromIastScan.API = make([]string, 0) cfg.Security.ScanControllers.IastScanRequestRateLimit = 3600 return cfg } // To completely disable security set NEW_RELIC_SECURITY_AGENT_ENABLED env to false. // If env is set to false,the security module is not loaded func isSecurityAgentEnabled() bool { if env := os.Getenv("NEW_RELIC_SECURITY_AGENT_ENABLED"); env != "" { if b, err := strconv.ParseBool(env); err == nil { return b } } return true } // InitSecurityAgent initializes the nrsecurityagent integration package from user-supplied // configuration values. func InitSecurityAgent(app *newrelic.Application, opts ...ConfigOption) error { if app == nil { return fmt.Errorf("Newrelic application value cannot be nil; did you call newrelic.NewApplication?") } c := defaultSecurityConfig() for _, fn := range opts { if fn != nil { fn(&c) if c.Error != nil { return c.Error } } } appConfig, isValid := app.Config() if !isValid { return fmt.Errorf("Newrelic application value cannot be read; did you call newrelic.NewApplication?") } app.UpdateSecurityConfig(c.Security) if !appConfig.HighSecurity && isSecurityAgentEnabled() { secureAgent := securityAgent.InitSecurityAgent(c.Security, appConfig.AppName, appConfig.License, appConfig.Logger.DebugEnabled()) app.RegisterSecurityAgent(secureAgent) } return nil } // ConfigOption functions are used to programmatically provide configuration values to the // nrsecurityagent integration package. type ConfigOption func(*SecurityConfig) // ConfigSecurityFromYaml directs the nrsecurityagent integration to read an external // YAML-formatted file to obtain its configuration values. // // The path to this file must be provided by setting the environment variable NEW_RELIC_SECURITY_CONFIG_PATH. func ConfigSecurityFromYaml() ConfigOption { return func(cfg *SecurityConfig) { confgFilePath := os.Getenv("NEW_RELIC_SECURITY_CONFIG_PATH") if confgFilePath == "" { cfg.Error = fmt.Errorf("Invalid value: NEW_RELIC_SECURITY_CONFIG_PATH can't be empty") return } data, err := ioutil.ReadFile(confgFilePath) if err == nil { err = yaml.Unmarshal(data, &cfg.Security) if err != nil { cfg.Error = fmt.Errorf("Error while interpreting config file \"%s\" value: %v", confgFilePath, err) return } } else { cfg.Error = fmt.Errorf("Error while reading config file \"%s\" , %v", confgFilePath, err) return } } } // ConfigSecurityFromEnvironment directs the nrsecurityagent integration to obtain all of its // configuration information from environment variables: // // NEW_RELIC_SECURITY_ENABLED (boolean) // NEW_RELIC_SECURITY_VALIDATOR_SERVICE_URL provides URL for the security validator service // NEW_RELIC_SECURITY_MODE scanning mode: "IAST" for now // NEW_RELIC_SECURITY_AGENT_ENABLED (boolean) // NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT (integer) set limit on read request body in kb. By default, this is "300" // NEW_RELIC_SECURITY_IAST_TEST_IDENTIFIER (string) This configuration allows users to specify a unique test identifier when running IAST Scan with CI/CD // // NEW_RELIC_SECURITY_SCAN_SCHEDULE_DELAY (integer) The delay field indicated time in minutes before the IAST scan starts after the application starts. By default is 0 min. // NEW_RELIC_SECURITY_SCAN_SCHEDULE_DURATION (integer) The duration field specifies the duration of the IAST scan in minutes. This determines how long the scan will run. By default is forever. // NEW_RELIC_SECURITY_SCAN_SCHEDULE_SCHEDULE (string) The schedule field specifies a cron expression that defines when the IAST scan should run. // NEW_RELIC_SECURITY_SCAN_SCHEDULE_ALWAYS_SAMPLE_TRACES (boolean) always_sample_traces permits IAST to actively gather trace data in the background, and the collected data will be used by Security Agent to perform an IAST Scan at the scheduled time. // NEW_RELIC_SECURITY_SCAN_CONTROLLERS_IAST_SCAN_REQUEST_RATE_LIMIT (integer) The IAST Scan Rate Limit settings limit the maximum number of analysis probes or requests that can be sent to the application in a minute, By default is 3600. // NEW_RELIC_SECURITY_SCAN_CONTROLLERS_SCAN_INSTANCE_COUNT (integer) This configuration allows users to the number of application instances for a specific entity where IAST analysis is performed. // // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INSECURE_SETTINGS (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INVALID_FILE_ACCESS (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SQL_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_NOSQL_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_LDAP_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_JAVASCRIPT_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_COMMAND_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_XPATH_INJECTION (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SSRF (boolean) // NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_RXSS (boolean) func ConfigSecurityFromEnvironment() ConfigOption { return func(cfg *SecurityConfig) { assignBool := func(field *bool, name string) { if env := os.Getenv(name); env != "" { if b, err := strconv.ParseBool(env); nil != err { cfg.Error = fmt.Errorf("invalid %s value: %s", name, env) } else { *field = b } } } assignString := func(field *string, name string) { if env := os.Getenv(name); env != "" { *field = env } } assignInt := func(field *int, name string) { if env := os.Getenv(name); env != "" { if i, err := strconv.Atoi(env); nil != err { cfg.Error = fmt.Errorf("invalid %s value: %s", name, env) } else { *field = i } } } assignBool(&cfg.Security.Enabled, "NEW_RELIC_SECURITY_ENABLED") assignString(&cfg.Security.Validator_service_url, "NEW_RELIC_SECURITY_VALIDATOR_SERVICE_URL") assignString(&cfg.Security.Mode, "NEW_RELIC_SECURITY_MODE") assignBool(&cfg.Security.Agent.Enabled, "NEW_RELIC_SECURITY_AGENT_ENABLED") assignBool(&cfg.Security.Detection.Rxss.Enabled, "NEW_RELIC_SECURITY_DETECTION_RXSS_ENABLED") assignInt(&cfg.Security.Request.BodyLimit, "NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT") assignString(&cfg.Security.IastTestIdentifier, "NEW_RELIC_SECURITY_IAST_TEST_IDENTIFIER") assignInt(&cfg.Security.ScanSchedule.Delay, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_DELAY") assignInt(&cfg.Security.ScanSchedule.Duration, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_DURATION") assignString(&cfg.Security.ScanSchedule.Schedule, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_SCHEDULE") assignBool(&cfg.Security.ScanSchedule.AllowIastSampleCollection, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_ALWAYS_SAMPLE_TRACES") assignInt(&cfg.Security.ScanControllers.IastScanRequestRateLimit, "NEW_RELIC_SECURITY_SCAN_CONTROLLERS_IAST_SCAN_REQUEST_RATE_LIMIT") assignInt(&cfg.Security.ScanControllers.ScanInstanceCount, "NEW_RELIC_SECURITY_SCAN_CONTROLLERS_SCAN_INSTANCE_COUNT") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.InsecureSettings, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INSECURE_SETTINGS") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.InvalidFileAccess, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INVALID_FILE_ACCESS") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.SQLInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SQL_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.NosqlInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_NOSQL_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.LdapInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_LDAP_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.JavascriptInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_JAVASCRIPT_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.CommandInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_COMMAND_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.XpathInjection, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_XPATH_INJECTION") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.Ssrf, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SSRF") assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.Rxss, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_RXSS") if env := os.Getenv("NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_API"); env != "" { cfg.Security.ExcludeFromIastScan.API = strings.Split(env, ",") } if env := os.Getenv("NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_HEADER"); env != "" { cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Header = strings.Split(env, ",") } if env := os.Getenv("NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_QUERY"); env != "" { cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Query = strings.Split(env, ",") } if env := os.Getenv("NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_BODY"); env != "" { cfg.Security.ExcludeFromIastScan.HttpRequestParameters.Body = strings.Split(env, ",") } } } // ConfigSecurityMode sets the security mode to use. By default, this is "IAST". func ConfigSecurityMode(mode string) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.Mode = mode } } // ConfigSecurityValidatorServiceEndPointUrl sets the security validator service endpoint. func ConfigSecurityValidatorServiceEndPointUrl(url string) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.Validator_service_url = url } } // ConfigSecurityIastTestIdentifier sets the iast test identifier. // This configuration allows users to specify a unique test identifier when running IAST Scan with CI/CD. func ConfigSecurityIastTestIdentifier(testIdentifier string) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.IastTestIdentifier = testIdentifier } } // ConfigSecurityDetectionDisableRxss is used to enable or disable RXSS validation. func ConfigSecurityDetectionDisableRxss(isDisable bool) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.Detection.Rxss.Enabled = !isDisable } } // ConfigSecurityEnable enables or disables the security integration. func ConfigSecurityEnable(isEnabled bool) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.Enabled = isEnabled } } // ConfigSecurityRequestBodyLimit set limit on read request body in kb. By default, this is "300" func ConfigSecurityRequestBodyLimit(bodyLimit int) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.Request.BodyLimit = bodyLimit } } // ConfigScanScheduleDelay is used to set delay for scan schedule. // The delay field indicated time in minutes before the IAST scan starts after the application starts func ConfigScanScheduleDelay(delay int) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanSchedule.Delay = delay } } // ConfigScanScheduleDuration is used to set duration for scan schedule. // The duration field specifies the duration of the IAST scan in minutes. This determines how long the scan will run. func ConfigScanScheduleDuration(duration int) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanSchedule.Duration = duration } } // ConfigScanScheduleSetSchedule is used to set schedule for scan schedule. // The schedule field specifies a cron expression that defines when the IAST scan should run. func ConfigScanScheduleSetSchedule(schedule string) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanSchedule.Schedule = schedule } } // ConfigScanScheduleAllowIastSampleCollection is used to allow or disallow IAST sample collection // always_sample_traces permits IAST to actively gather trace data in the background, and the collected data will be used by Security Agent to perform an IAST Scan at the scheduled time. func ConfigScanScheduleAllowIastSampleCollection(isAllowed bool) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanSchedule.AllowIastSampleCollection = isAllowed } } // ConfigScanControllersIastScanRequestRateLimit is used to set IAST scan request rate limit. // The IAST Scan Rate Limit settings limit the maximum number of analysis probes or requests that can be sent to the application in a minute func ConfigIastScanRequestRateLimit(limit int) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanControllers.IastScanRequestRateLimit = limit } } // ConfigScanIstanceCount is used to set scan instance count. // This configuration allows users to the number of application instances for a specific entity where IAST analysis is performed. func ConfigScanInstanceCount(limit int) ConfigOption { return func(cfg *SecurityConfig) { cfg.Security.ScanControllers.ScanInstanceCount = limit } } go-agent-3.42.0/v3/integrations/nrslog/000077500000000000000000000000001510742411500176435ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrslog/LICENSE.txt000066400000000000000000000264501510742411500214750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrslog/README.md000066400000000000000000000006301510742411500211210ustar00rootroot00000000000000# v3/integrations/nrslog [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrslog?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrslog) Package `nrslog` supports `log/slog`. ```go import "github.com/newrelic/go-agent/v3/integrations/nrslog" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrslog). go-agent-3.42.0/v3/integrations/nrslog/example_test.go000066400000000000000000000070211510742411500226640ustar00rootroot00000000000000package nrslog_test import ( "bytes" "log/slog" "testing" "github.com/stretchr/testify/assert" "github.com/newrelic/go-agent/v3/integrations/nrslog" "github.com/newrelic/go-agent/v3/newrelic" ) func Example() { // Get the default logger or create a new one: l := slog.Default() _, err := newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEWRELIC_LICENSE_KEY__"), // Use nrslog to register the logger with the agent: nrslog.ConfigLogger(l.WithGroup("newrelic")), ) if err != nil { panic(err) } } func TestLogs(t *testing.T) { type args struct { message string EnabledLevel slog.Level } tests := []struct { name string args args logFunc func(logger newrelic.Logger, message string, attrs map[string]interface{}) want string }{ { name: "Error", args: args{ message: "error message", EnabledLevel: slog.LevelError, }, logFunc: newrelic.Logger.Error, want: "level=ERROR msg=\"error message\" key=val\n", }, { name: "Warn", args: args{ message: "warning message", EnabledLevel: slog.LevelWarn, }, logFunc: newrelic.Logger.Warn, want: "level=WARN msg=\"warning message\" key=val\n", }, { name: "Info", args: args{ message: "informational message", EnabledLevel: slog.LevelInfo, }, logFunc: newrelic.Logger.Info, want: "level=INFO msg=\"informational message\" key=val\n", }, { name: "Debug", args: args{ message: "debug message", EnabledLevel: slog.LevelDebug, }, logFunc: newrelic.Logger.Debug, want: "level=DEBUG msg=\"debug message\" key=val\n", }, { name: "Disabled", args: args{ message: "disabled message", EnabledLevel: slog.LevelError, }, logFunc: newrelic.Logger.Debug, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create slog to record logs at the specified level: buf := new(bytes.Buffer) handler := slog.NewTextHandler(buf, &slog.HandlerOptions{ Level: tt.args.EnabledLevel, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { // Remove time from the output for predictable test output. if a.Key == slog.TimeKey { return slog.Attr{} } return a }, }) logger := slog.New(handler) // Create test logger using nrslog.Transform: testLogger := nrslog.Transform(logger) // Define attributes for the test log message: attrs := map[string]interface{}{ "key": "val", } // Log the message and attributes using the test logger: tt.logFunc(testLogger, tt.args.message, attrs) assert.Equal(t, tt.want, buf.String()) }) } } func TestDebugEnabled(t *testing.T) { type args struct { EnabledLevel slog.Level } tests := []struct { name string args args want bool }{ { name: "Debug", args: args{ EnabledLevel: slog.LevelDebug, }, want: true, }, { name: "Info", args: args{ EnabledLevel: slog.LevelInfo, }, want: false, }, { name: "Warn", args: args{ EnabledLevel: slog.LevelWarn, }, want: false, }, { name: "Error", args: args{ EnabledLevel: slog.LevelError, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := slog.NewJSONHandler( new(bytes.Buffer), &slog.HandlerOptions{Level: tt.args.EnabledLevel}, ) logger := slog.New(handler) testLogger := nrslog.Transform(logger) assert.Equal(t, tt.want, testLogger.DebugEnabled()) }) } } go-agent-3.42.0/v3/integrations/nrslog/go.mod000066400000000000000000000004521510742411500207520ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrslog // The new log/slog package in Go 1.21 brings structured logging to the standard library. go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/stretchr/testify v1.9.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrslog/nrslog.go000066400000000000000000000026541510742411500215050ustar00rootroot00000000000000// Package nrslog supports `log/slog` // // Wrap your slog Logger using nrslog.Transform to send agent log messages to standard log library. package nrslog import ( "context" "log/slog" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" ) func init() { internal.TrackUsage("integration", "logging", "slog") } func transformAttributes(c map[string]interface{}) []any { attrs := make([]any, 0, len(c)) for k, v := range c { attrs = append(attrs, slog.Any(k, v)) } return attrs } type shim struct{ logger *slog.Logger } func (s *shim) Error(msg string, c map[string]interface{}) { s.logger.Error(msg, transformAttributes(c)...) } func (s *shim) Warn(msg string, c map[string]interface{}) { s.logger.Warn(msg, transformAttributes(c)...) } func (s *shim) Info(msg string, c map[string]interface{}) { s.logger.Info(msg, transformAttributes(c)...) } func (s *shim) Debug(msg string, c map[string]interface{}) { s.logger.Debug(msg, transformAttributes(c)...) } func (s *shim) DebugEnabled() bool { return s.logger.Enabled(context.Background(), slog.LevelDebug) } // Transform turns a *slog.Logger into a newrelic.Logger. func Transform(l *slog.Logger) newrelic.Logger { return &shim{logger: l} } // ConfigLogger configures the newrelic.Application to send log messages to the // provided slog. func ConfigLogger(l *slog.Logger) newrelic.ConfigOption { return newrelic.ConfigLogger(Transform(l)) } go-agent-3.42.0/v3/integrations/nrsnowflake/000077500000000000000000000000001510742411500206705ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsnowflake/LICENSE.txt000066400000000000000000000264501510742411500225220ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrsnowflake/README.md000066400000000000000000000007311510742411500221500ustar00rootroot00000000000000# v3/integrations/nrsnowflake [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsnowflake?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsnowflake) Package `nrsnowflake` instruments https://github.com/snowflakedb/gosnowflake. ```go import "github.com/newrelic/go-agent/v3/integrations/nrsnowflake" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsnowflake). go-agent-3.42.0/v3/integrations/nrsnowflake/example/000077500000000000000000000000001510742411500223235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsnowflake/example/main.go000066400000000000000000000023241510742411500235770ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "database/sql" "fmt" "log" "os" "time" // 1. Instead of importing github.com/snowflakedb/gosnowflake, import the // nrsnowflake integration _ "github.com/newrelic/go-agent/v3/integrations/nrsnowflake" "github.com/newrelic/go-agent/v3/newrelic" ) func main() { // 2. Instead of opening "snowflake", open "nrsnowflake" db, err := sql.Open("nrsnowflake", "root@/information_schema") if err != nil { panic(err) } app, err := newrelic.NewApplication( newrelic.ConfigAppName("Snowflake app"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { log.Fatal(err) } app.WaitForConnection(5 * time.Second) defer app.Shutdown(5 * time.Second) txn := app.StartTransaction("snowflakeQuery") defer txn.End() // 3. Add the transaction to the context ctx := newrelic.NewContext(context.Background(), txn) // 4. Call methods on the db using the context row := db.QueryRowContext(ctx, "SELECT count(*) from tables") var count int row.Scan(&count) fmt.Println("number of tables in information_schema", count) } go-agent-3.42.0/v3/integrations/nrsnowflake/go.mod000066400000000000000000000003611510742411500217760ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrsnowflake go 1.24 toolchain go1.23.2 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/snowflakedb/gosnowflake v1.14.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrsnowflake/nrsnowflake.go000066400000000000000000000047551510742411500235630ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrsnowflake instruments github.com/snowflakedb/gosnowflake // // Use this package to instrument your Snowflake calls without having to manually // create DatastoreSegments. This is done in a two step process: // // 1. Use this package's driver in place of the snowflake driver. // // If your code is using sql.Open like this: // // import ( // _ "github.com/snowflakedb/gosnowflake" // ) // // func main() { // db, err := sql.Open("snowflake", "user@unix(/path/to/socket)/dbname") // } // // Then change the side-effect import to this package, and open "nrsnowflake" instead: // // import ( // _ "github.com/newrelic/go-agent/v3/integrations/nrsnowflake" // ) // // func main() { // db, err := sql.Open("nrsnowflake", "user@unix(/path/to/socket)/dbname") // } // // 2. Provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) from tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) from tables") // // A working example is shown here: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsnowflake/example/main.go package nrsnowflake import ( "database/sql" "strconv" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" "github.com/snowflakedb/gosnowflake" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastoreSnowflake, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN, } ) func init() { sql.Register("nrsnowflake", newrelic.InstrumentSQLDriver(gosnowflake.SnowflakeDriver{}, baseBuilder)) internal.TrackUsage("integration", "driver", "snowflake") } func parseDSN(s *newrelic.DatastoreSegment, dsn string) { cfg, err := gosnowflake.ParseDSN(dsn) if nil != err { return } parseConfig(s, cfg) } func parseConfig(s *newrelic.DatastoreSegment, cfg *gosnowflake.Config) { sPort := strconv.Itoa(cfg.Port) if cfg.Port == 0 { sPort = "" } s.DatabaseName = cfg.Database s.Host = cfg.Host s.PortPathOrID = sPort } go-agent-3.42.0/v3/integrations/nrsnowflake/nrsnowflake_test.go000066400000000000000000000044631510742411500246160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrsnowflake import ( "testing" "github.com/newrelic/go-agent/v3/newrelic" "github.com/snowflakedb/gosnowflake" ) func TestParseDSN(t *testing.T) { testcases := []struct { dsn string expHost string expPortPathOrID string expDatabaseName string }{ { dsn: "user:password@account/database/schema", expHost: "account.snowflakecomputing.com", expPortPathOrID: "443", expDatabaseName: "database", }, { dsn: "user:password@host:123/database/schema?account=user_account", expHost: "host", expPortPathOrID: "123", expDatabaseName: "database", }, } for _, test := range testcases { s := &newrelic.DatastoreSegment{} parseDSN(s, test.dsn) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } func TestParseConfig(t *testing.T) { testcases := []struct { cfgHost string cfgPort int cfgDBName string expHost string expPortPathOrID string expDatabaseName string }{ { cfgDBName: "mydb", expDatabaseName: "mydb", expPortPathOrID: "", }, { cfgHost: "unixgram", cfgPort: 123, expHost: "unixgram", expPortPathOrID: "123", }, } for _, test := range testcases { s := &newrelic.DatastoreSegment{} cfg := &gosnowflake.Config{ Host: test.cfgHost, Port: test.cfgPort, Database: test.cfgDBName, } parseConfig(s, cfg) if test.expHost != s.Host { t.Errorf(`incorrect host, expected="%s", actual="%s"`, test.expHost, s.Host) } if test.expPortPathOrID != s.PortPathOrID { t.Errorf(`incorrect port path or id, expected="%s", actual="%s"`, test.expPortPathOrID, s.PortPathOrID) } if test.expDatabaseName != s.DatabaseName { t.Errorf(`incorrect database name, expected="%s", actual="%s"`, test.expDatabaseName, s.DatabaseName) } } } go-agent-3.42.0/v3/integrations/nrsqlite3/000077500000000000000000000000001510742411500202635ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsqlite3/LICENSE.txt000066400000000000000000000264501510742411500221150ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrsqlite3/README.md000066400000000000000000000007061510742411500215450ustar00rootroot00000000000000# v3/integrations/nrsqlite3 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsqlite3?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsqlite3) Package `nrsqlite3` instruments https://github.com/mattn/go-sqlite3. ```go import "github.com/newrelic/go-agent/v3/integrations/nrsqlite3" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrsqlite3). go-agent-3.42.0/v3/integrations/nrsqlite3/example/000077500000000000000000000000001510742411500217165ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrsqlite3/example/main.go000066400000000000000000000030531510742411500231720ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "database/sql" "fmt" "os" "time" _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func main() { db, err := sql.Open("nrsqlite3", ":memory:") if err != nil { panic(err) } defer db.Close() db.Exec("CREATE TABLE zaps ( zap_num INTEGER )") db.Exec("INSERT INTO zaps (zap_num) VALUES (22)") app, err := newrelic.NewApplication( newrelic.ConfigAppName("SQLite App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if err != nil { panic(err) } app.WaitForConnection(10 * time.Second) txn := app.StartTransaction("sqliteQuery") ctx := newrelic.NewContext(context.Background(), txn) row := db.QueryRowContext(ctx, "SELECT count(*) from zaps") var count int row.Scan(&count) txn.End() txn = app.StartTransaction("CustomSQLQuery") s := newrelic.DatastoreSegment{ Product: newrelic.DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: map[string]interface{}{ "name": "Dracula", "age": 439, }, Host: "mysql-server-1", PortPathOrID: "3306", DatabaseName: "my_database", } s.StartTime = txn.StartSegmentNow() // ... do the operation s.End() txn.End() app.Shutdown(5 * time.Second) fmt.Printf("number of elements in table: %v\n", count) } go-agent-3.42.0/v3/integrations/nrsqlite3/go.mod000066400000000000000000000006171510742411500213750ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrsqlite3 // As of Dec 2019, 1.9 is the oldest version of Go tested by go-sqlite3: // https://github.com/mattn/go-sqlite3/blob/master/.travis.yml go 1.24 require ( github.com/mattn/go-sqlite3 v1.0.0 // v3.3.0 includes the new location of ParseQuery github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrsqlite3/nrsqlite3.go000066400000000000000000000077631510742411500225530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 // Package nrsqlite3 instruments https://github.com/mattn/go-sqlite3. // // Use this package to instrument your SQLite calls without having to manually // create DatastoreSegments. This is done in a two step process: // // 1. Use this package's driver in place of the sqlite3 driver. // // If your code is using sql.Open like this: // // import ( // _ "github.com/mattn/go-sqlite3" // ) // // func main() { // db, err := sql.Open("sqlite3", "./foo.db") // } // // Then change the side-effect import to this package, and open "nrsqlite3" instead: // // import ( // _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3" // ) // // func main() { // db, err := sql.Open("nrsqlite3", "./foo.db") // } // // If you are registering a custom sqlite3 driver with special behavior then // you must wrap your driver instance using nrsqlite3.InstrumentSQLDriver. For // example, if your code looks like this: // // func main() { // sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ // Extensions: []string{ // "sqlite3_mod_regexp", // }, // }) // db, err := sql.Open("sqlite3_with_extensions", ":memory:") // } // // Then instrument the driver like this: // // func main() { // sql.Register("sqlite3_with_extensions", nrsqlite3.InstrumentSQLDriver(&sqlite3.SQLiteDriver{ // Extensions: []string{ // "sqlite3_mod_regexp", // }, // })) // db, err := sql.Open("sqlite3_with_extensions", ":memory:") // } // // 2. Provide a context containing a newrelic.Transaction to all exec and query // methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt. This requires using the // context methods ExecContext, QueryContext, and QueryRowContext in place of // Exec, Query, and QueryRow respectively. For example, instead of the // following: // // row := db.QueryRow("SELECT count(*) from tables") // // Do this: // // ctx := newrelic.NewContext(context.Background(), txn) // row := db.QueryRowContext(ctx, "SELECT count(*) from tables") // // A working example is shown here: // https://github.com/newrelic/go-agent/tree/master/v3/integrations/nrsqlite3/example/main.go package nrsqlite3 import ( "database/sql" "database/sql/driver" "path/filepath" "strings" sqlite3 "github.com/mattn/go-sqlite3" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/sqlparse" ) var ( baseBuilder = newrelic.SQLDriverSegmentBuilder{ BaseSegment: newrelic.DatastoreSegment{ Product: newrelic.DatastoreSQLite, }, ParseQuery: sqlparse.ParseQuery, ParseDSN: parseDSN, } ) func init() { sql.Register("nrsqlite3", InstrumentSQLDriver(&sqlite3.SQLiteDriver{})) internal.TrackUsage("integration", "driver", "sqlite3") } // InstrumentSQLDriver wraps an sqlite3.SQLiteDriver to add instrumentation. // For example, if you are registering a custom SQLiteDriver like this: // // sql.Register("sqlite3_with_extensions", // &sqlite3.SQLiteDriver{ // Extensions: []string{ // "sqlite3_mod_regexp", // }, // }) // // Then add instrumentation like this: // // sql.Register("sqlite3_with_extensions", // nrsqlite3.InstrumentSQLDriver(&sqlite3.SQLiteDriver{ // Extensions: []string{ // "sqlite3_mod_regexp", // }, // })) func InstrumentSQLDriver(d *sqlite3.SQLiteDriver) driver.Driver { return newrelic.InstrumentSQLDriver(d, baseBuilder) } func getPortPathOrID(dsn string) (ppoid string) { ppoid = strings.Split(dsn, "?")[0] ppoid = strings.TrimPrefix(ppoid, "file:") if ":memory:" != ppoid && "" != ppoid { if abs, err := filepath.Abs(ppoid); nil == err { ppoid = abs } } return } // ParseDSN accepts a DSN string and sets the Host, PortPathOrID, and // DatabaseName fields on a newrelic.DatastoreSegment. func parseDSN(s *newrelic.DatastoreSegment, dsn string) { // See https://godoc.org/github.com/mattn/go-sqlite3#SQLiteDriver.Open s.Host = "localhost" s.PortPathOrID = getPortPathOrID(dsn) s.DatabaseName = "" } go-agent-3.42.0/v3/integrations/nrsqlite3/nrsqlite3_test.go000066400000000000000000000013321510742411500235740ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrsqlite3 import ( "path/filepath" "runtime" "testing" ) func TestGetPortPathOrID(t *testing.T) { _, here, _, _ := runtime.Caller(0) currentDir := filepath.Dir(here) testcases := []struct { dsn string expected string }{ {":memory:", ":memory:"}, {"test.db", filepath.Join(currentDir, "test.db")}, {"file:/test.db?cache=shared&mode=memory", "/test.db"}, {"file::memory:", ":memory:"}, {"", ""}, } for _, test := range testcases { if actual := getPortPathOrID(test.dsn); actual != test.expected { t.Errorf(`incorrect port path or id: dsn="%s", actual="%s"`, test.dsn, actual) } } } go-agent-3.42.0/v3/integrations/nrstan/000077500000000000000000000000001510742411500176445ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrstan/LICENSE.txt000066400000000000000000000264501510742411500214760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrstan/README.md000066400000000000000000000006631510742411500211300ustar00rootroot00000000000000# v3/integrations/nrstan [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrstan?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrstan) Package `nrstan` instruments https://github.com/nats-io/stan.go. ```go import "github.com/newrelic/go-agent/v3/integrations/nrstan" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrstan). go-agent-3.42.0/v3/integrations/nrstan/examples/000077500000000000000000000000001510742411500214625ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrstan/examples/LICENSE.txt000066400000000000000000000264501510742411500233140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrstan/examples/README.md000066400000000000000000000005731510742411500227460ustar00rootroot00000000000000# Example STAN app In this example app you can find several different ways of instrumenting NATS Streaming functions using New Relic. In order to run the app, make sure the following assumptions are correct: * Your New Relic license key is available as an environment variable named `NEW_RELIC_LICENSE_KEY` * A NATS Streaming Server is running with the cluster id `test-cluster`go-agent-3.42.0/v3/integrations/nrstan/examples/go.mod000066400000000000000000000010361510742411500225700ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrstan/examples // This module exists to avoid a dependency on nrnrats. go 1.24 require ( github.com/nats-io/stan.go v0.10.4 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrnats v0.0.0 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrstan => ../ replace github.com/newrelic/go-agent/v3/integrations/nrnats => ../../nrnats/ replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/nrstan/examples/main.go000066400000000000000000000046651510742411500227500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "sync" "time" stan "github.com/nats-io/stan.go" "github.com/newrelic/go-agent/v3/integrations/nrnats" "github.com/newrelic/go-agent/v3/integrations/nrstan" "github.com/newrelic/go-agent/v3/newrelic" ) var app *newrelic.Application func doAsync(sc stan.Conn, txn *newrelic.Transaction) { wg := sync.WaitGroup{} subj := "async" // Simple Async Subscriber // Use the nrstan.StreamingSubWrapper to wrap the stan.MsgHandler and // create a newrelic.Transaction with each processed stan.Msg _, err := sc.Subscribe(subj, nrstan.StreamingSubWrapper(app, func(m *stan.Msg) { defer wg.Done() fmt.Println("Received async message:", string(m.Data)) })) if nil != err { panic(err) } // Simple Publisher wg.Add(1) // Use nrnats.StartPublishSegment to create a newrelic.ExternalSegment for // the call to sc.Publish seg := nrnats.StartPublishSegment(txn, sc.NatsConn(), subj) err = sc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } wg.Wait() } func doQueue(sc stan.Conn, txn *newrelic.Transaction) { wg := sync.WaitGroup{} subj := "queue" // Queue Subscriber // Use the nrstan.StreamingSubWrapper to wrap the stan.MsgHandler and // create a newrelic.Transaction with each processed stan.Msg _, err := sc.QueueSubscribe(subj, "myqueue", nrstan.StreamingSubWrapper(app, func(m *stan.Msg) { defer wg.Done() fmt.Println("Received queue message:", string(m.Data)) })) if nil != err { panic(err) } wg.Add(1) // Use nrnats.StartPublishSegment to create a newrelic.ExternalSegment for // the call to sc.Publish seg := nrnats.StartPublishSegment(txn, sc.NatsConn(), subj) err = sc.Publish(subj, []byte("Hello World")) seg.End() if nil != err { panic(err) } wg.Wait() } func main() { // Initialize agent var err error app, err = newrelic.NewApplication( newrelic.ConfigAppName("STAN App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { panic(err) } defer app.Shutdown(10 * time.Second) err = app.WaitForConnection(5 * time.Second) if nil != err { panic(err) } txn := app.StartTransaction("main") defer txn.End() // Connect to a server sc, err := stan.Connect("test-cluster", "clientid") if nil != err { panic(err) } defer sc.Close() doAsync(sc, txn) doQueue(sc, txn) } go-agent-3.42.0/v3/integrations/nrstan/go.mod000066400000000000000000000005461510742411500207570ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrstan // As of Dec 2019, 1.11 is the earliest Go version tested by Stan: // https://github.com/nats-io/stan.go/blob/master/.travis.yml go 1.24 toolchain go1.24.2 require ( github.com/nats-io/stan.go v0.10.4 github.com/newrelic/go-agent/v3 v3.42.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrstan/nrstan.go000066400000000000000000000024051510742411500215010ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrstan import ( stan "github.com/nats-io/stan.go" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) // StreamingSubWrapper can be used to wrap the function for STREAMING stan.Subscribe and stan.QueueSubscribe // (https://godoc.org/github.com/nats-io/stan.go#Conn) // If the `newrelic.Application` parameter is non-nil, it will create a `newrelic.Transaction` and end the transaction // when the passed function is complete. func StreamingSubWrapper(app *newrelic.Application, f func(msg *stan.Msg)) func(msg *stan.Msg) { if app == nil { return f } return func(msg *stan.Msg) { namer := internal.MessageMetricKey{ Library: "STAN", DestinationType: string(newrelic.MessageTopic), DestinationName: msg.MsgProto.Subject, Consumer: true, } txn := app.StartTransaction(namer.Name()) defer txn.End() integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageRoutingKey, msg.MsgProto.Subject, nil) integrationsupport.AddAgentAttribute(txn, newrelic.AttributeMessageReplyTo, msg.MsgProto.Reply, nil) f(msg) } } go-agent-3.42.0/v3/integrations/nrstan/nrstan_doc.go000066400000000000000000000037021510742411500223270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrstan instruments https://github.com/nats-io/stan.go. // // This package can be used to simplify instrumenting NATS Streaming subscribers. Currently due to the nature of // the NATS Streaming framework we are limited to two integration points: `StartPublishSegment` for publishers, and // `SubWrapper` for subscribers. // // # NATS Streaming subscribers // // `nrstan.StreamingSubWrapper` can be used to wrap the function for STREAMING stan.Subscribe and stan.QueueSubscribe // (https://godoc.org/github.com/nats-io/stan.go#Conn) If the `newrelic.Application` parameter is non-nil, it will // create a `newrelic.Transaction` and end the transaction when the passed function is complete. Example: // // sc, err := stan.Connect(clusterName, clientName) // if err != nil { // t.Fatal("Couldn't connect to server", err) // } // defer sc.Close() // app := createTestApp(t) // newrelic.Application // sc.Subscribe(subject, StreamingSubWrapper(app, myMessageHandler) // // # NATS Streaming publishers // // You can use `nrnats.StartPublishSegment` from the `nrnats` package // (https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrnats/#StartPublishSegment) // to start an external segment when doing a streaming publish, which must be ended after publishing is complete. // Example: // // sc, err := stan.Connect(clusterName, clientName) // if err != nil { // t.Fatal("Couldn't connect to server", err) // } // txn := currentTransaction() // current newrelic.Transaction // seg := nrnats.StartPublishSegment(txn, sc.NatsConn(), subj) // sc.Publish(subj, []byte("Hello World")) // seg.End() // // Full Publisher/Subscriber example: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrstan/examples/main.go package nrstan import "github.com/newrelic/go-agent/v3/internal" func init() { internal.TrackUsage("integration", "framework", "stan") } go-agent-3.42.0/v3/integrations/nrstan/test/000077500000000000000000000000001510742411500206235ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrstan/test/LICENSE.txt000066400000000000000000000264501510742411500224550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrstan/test/go.mod000066400000000000000000000010051510742411500217250ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrstan/test // This module exists to avoid a dependency on // github.com/nats-io/nats-streaming-server in nrstan. go 1.24 toolchain go1.24.2 require ( github.com/nats-io/nats-streaming-server v0.25.6 github.com/nats-io/stan.go v0.10.4 github.com/newrelic/go-agent/v3 v3.42.0 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrstan => ../ replace github.com/newrelic/go-agent/v3 => ../../.. go-agent-3.42.0/v3/integrations/nrstan/test/nrstan_test.go000066400000000000000000000064711510742411500235260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package test import ( "os" "sync" "testing" "github.com/nats-io/nats-streaming-server/server" stan "github.com/nats-io/stan.go" "github.com/newrelic/go-agent/v3/integrations/nrstan" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" ) const ( clusterName = "my_test_cluster" clientName = "me" ) func TestMain(m *testing.M) { s, err := server.RunServer(clusterName) if err != nil { panic(err) } defer s.Shutdown() os.Exit(m.Run()) } func createTestApp() integrationsupport.ExpectApp { return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var cfgFn = func(cfg *newrelic.Config) { cfg.Attributes.Include = append(cfg.Attributes.Include, newrelic.AttributeMessageRoutingKey, newrelic.AttributeMessageQueueName, newrelic.AttributeMessageExchangeType, newrelic.AttributeMessageReplyTo, newrelic.AttributeMessageCorrelationID, ) } func TestSubWrapperWithNilApp(t *testing.T) { subject := "sample.subject1" sc, err := stan.Connect(clusterName, clientName) if err != nil { t.Fatal("Couldn't connect to server", err) } defer sc.Close() wg := sync.WaitGroup{} sc.Subscribe(subject, nrstan.StreamingSubWrapper(nil, func(msg *stan.Msg) { defer wg.Done() })) wg.Add(1) sc.Publish(subject, []byte("data")) wg.Wait() } func TestSubWrapper(t *testing.T) { subject := "sample.subject2" sc, err := stan.Connect(clusterName, clientName) if err != nil { t.Fatal("Couldn't connect to server", err) } defer sc.Close() wg := sync.WaitGroup{} app := createTestApp() sc.Subscribe(subject, WgWrapper(&wg, nrstan.StreamingSubWrapper(app.Application, func(msg *stan.Msg) {}))) wg.Add(1) sc.Publish(subject, []byte("data")) wg.Wait() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/Message/STAN/Topic/Named/sample.subject2", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/Message/STAN/Topic/Named/sample.subject2", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/Message/STAN/Topic/Named/sample.subject2", "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "message.routingKey": "sample.subject2", }, UserAttributes: map[string]interface{}{}, }, }) } // Wrapper function to ensure that the NR wrapper is done recording transaction data before wg.Done() is called func WgWrapper(wg *sync.WaitGroup, nrWrap func(msg *stan.Msg)) func(msg *stan.Msg) { return func(msg *stan.Msg) { nrWrap(msg) wg.Done() } } go-agent-3.42.0/v3/integrations/nrzap/000077500000000000000000000000001510742411500174715ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrzap/LICENSE.txt000066400000000000000000000264501510742411500213230ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrzap/README.md000066400000000000000000000006461510742411500207560ustar00rootroot00000000000000# v3/integrations/nrzap [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap) Package `nrzap` supports https://github.com/uber-go/zap. ```go import "github.com/newrelic/go-agent/v3/integrations/nrzap" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap). go-agent-3.42.0/v3/integrations/nrzap/example_test.go000066400000000000000000000050261510742411500225150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrzap_test import ( "testing" "github.com/newrelic/go-agent/v3/integrations/nrzap" "github.com/newrelic/go-agent/v3/newrelic" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) func Example() { // Create a new zap logger: z, _ := zap.NewProduction() newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEWRELIC_LICENSE_KEY__"), // Use nrzap to register the logger with the agent: nrzap.ConfigLogger(z.Named("newrelic")), ) } func TestLogs(t *testing.T) { tests := []struct { name string logFunc func(logger newrelic.Logger, message string, attrs map[string]interface{}) level zapcore.LevelEnabler }{ { name: "Error", logFunc: newrelic.Logger.Error, level: zap.ErrorLevel, }, { name: "Warn", logFunc: newrelic.Logger.Warn, level: zap.WarnLevel, }, { name: "Info", logFunc: newrelic.Logger.Info, level: zap.InfoLevel, }, { name: "Debug", logFunc: newrelic.Logger.Debug, level: zap.DebugLevel, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // Create an observer to record logs at the specified level: observedZapCore, observedLogs := observer.New(test.level) observedLogger := zap.New(observedZapCore) // Create a test logger using nrzap.Transform: testLogger := nrzap.Transform(observedLogger) // Define a message and attributes for the test log message: message := test.name attrs := map[string]interface{}{ "key": "val", } // Log the message and attributes using the test logger: test.logFunc(testLogger, message, attrs) // Check if observed log matches the expected message and attributes: logs := observedLogs.All() if len(logs) == 0 { t.Errorf("no log messages produced") } else { log := logs[0] if message != log.Message { t.Errorf("incorrect log message; expected: %s, got: %s", message, log.Message) } context := log.ContextMap() val, ok := context["key"] if !ok || val != "val" { t.Errorf("incorrect log attributes for key, \"key\"; expected \"val\", got: %s", val.(string)) } } }) } } func TestDebugEnabled(t *testing.T) { observedZapCore, _ := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) testLogger := nrzap.Transform(observedLogger) if !testLogger.DebugEnabled() { t.Errorf("debug logging is not enabled") } } go-agent-3.42.0/v3/integrations/nrzap/go.mod000066400000000000000000000005521510742411500206010ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrzap // As of Dec 2019, zap has 1.13 in their go.mod file: // https://github.com/uber-go/zap/blob/master/go.mod go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 // v1.12.0 is the earliest version of zap using modules. go.uber.org/zap v1.12.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrzap/nrzap.go000066400000000000000000000030501510742411500211500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrzap supports https://github.com/uber-go/zap // // Wrap your zap Logger using nrzap.Transform to send agent log messages to zap. package nrzap import ( "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" "go.uber.org/zap" ) func init() { internal.TrackUsage("integration", "logging", "zap") } type shim struct{ logger *zap.Logger } func transformAttributes(atts map[string]interface{}) []zap.Field { fs := make([]zap.Field, 0, len(atts)) for key, val := range atts { fs = append(fs, zap.Any(key, val)) } return fs } func (s *shim) Error(msg string, c map[string]interface{}) { s.logger.Error(msg, transformAttributes(c)...) } func (s *shim) Warn(msg string, c map[string]interface{}) { s.logger.Warn(msg, transformAttributes(c)...) } func (s *shim) Info(msg string, c map[string]interface{}) { s.logger.Info(msg, transformAttributes(c)...) } func (s *shim) Debug(msg string, c map[string]interface{}) { s.logger.Debug(msg, transformAttributes(c)...) } func (s *shim) DebugEnabled() bool { ce := s.logger.Check(zap.DebugLevel, "debugging") return ce != nil } // Transform turns a *zap.Logger into a newrelic.Logger. func Transform(l *zap.Logger) newrelic.Logger { return &shim{logger: l} } // ConfigLogger configures the newrelic.Application to send log messsages to the // provided zap logger. func ConfigLogger(l *zap.Logger) newrelic.ConfigOption { return newrelic.ConfigLogger(Transform(l)) } go-agent-3.42.0/v3/integrations/nrzerolog/000077500000000000000000000000001510742411500203605ustar00rootroot00000000000000go-agent-3.42.0/v3/integrations/nrzerolog/LICENSE.md000066400000000000000000000264501510742411500217730ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/integrations/nrzerolog/README.md000066400000000000000000000006741510742411500216460ustar00rootroot00000000000000# v3/integrations/nrzerolog [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog) Package `nrzerolog` supports https://github.com/rs/zerolog ```go import "github.com/newrelic/go-agent/v3/integrations/nrzerolog" ``` For more information, see [godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog). go-agent-3.42.0/v3/integrations/nrzerolog/example_test.go000066400000000000000000000010751510742411500234040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package nrzerolog_test import ( "os" "github.com/newrelic/go-agent/v3/integrations/nrzerolog" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) func Example() { // Create a new zerolog logger: zl := zerolog.New(os.Stderr) newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEWRELIC_LICENSE_KEY__"), // Use nrzerolog to register the logger with the agent: nrzerolog.ConfigLogger(&zl), ) } go-agent-3.42.0/v3/integrations/nrzerolog/go.mod000066400000000000000000000003151510742411500214650ustar00rootroot00000000000000module github.com/newrelic/go-agent/v3/integrations/nrzerolog go 1.24 require ( github.com/newrelic/go-agent/v3 v3.42.0 github.com/rs/zerolog v1.28.0 ) replace github.com/newrelic/go-agent/v3 => ../.. go-agent-3.42.0/v3/integrations/nrzerolog/nrzerolog.go000066400000000000000000000025221510742411500227310ustar00rootroot00000000000000// Copyright 2021 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package nrzerolog supports https://github.com/rs/zerolog // // Wrap your zerolog Logger using nrzerolog.Transform to send agent log messages to zerolog. package nrzerolog import ( "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" ) func init() { internal.TrackUsage("integration", "logging", "zerolog") } type shim struct{ logger *zerolog.Logger } func (s *shim) Error(msg string, c map[string]interface{}) { s.logger.Error().Fields(c).Msg(msg) } func (s *shim) Warn(msg string, c map[string]interface{}) { s.logger.Warn().Fields(c).Msg(msg) } func (s *shim) Info(msg string, c map[string]interface{}) { s.logger.Info().Fields(c).Msg(msg) } func (s *shim) Debug(msg string, c map[string]interface{}) { s.logger.Debug().Fields(c).Msg(msg) } func (s *shim) DebugEnabled() bool { return s.logger.GetLevel() == zerolog.DebugLevel } // Transform turns a *zerolog.Logger into a newrelic.Logger. func Transform(l *zerolog.Logger) newrelic.Logger { return &shim{logger: l} } // ConfigLogger configures the newrelic.Application to send log messsages to the // provided zerolog logger. func ConfigLogger(l *zerolog.Logger) newrelic.ConfigOption { return newrelic.ConfigLogger(Transform(l)) } go-agent-3.42.0/v3/internal/000077500000000000000000000000001510742411500154455ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/awssupport/000077500000000000000000000000001510742411500176745ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/awssupport/awssupport.go000066400000000000000000000063401510742411500224550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.8 // +build go1.8 package awssupport import ( "context" "github.com/newrelic/go-agent/v3/newrelic/integrationsupport" "net/http" "reflect" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) type contextKeyType struct{} var segmentContextKey = contextKeyType(struct{}{}) type endable interface{ End() } func getTableName(params interface{}) string { var tableName string v := reflect.ValueOf(params) if v.IsValid() && v.Kind() == reflect.Ptr { e := v.Elem() if e.Kind() == reflect.Struct { n := e.FieldByName("TableName") if n.IsValid() { if name, ok := n.Interface().(*string); ok { if nil != name { tableName = *name } } } } } return tableName } // GetRequestID looks for the AWS request ID header. func GetRequestID(hdr http.Header) string { id := hdr.Get("X-Amzn-Requestid") if id == "" { // Alternative version of request id in the header id = hdr.Get("X-Amz-Request-Id") } return id } // StartSegmentInputs is used as the input to StartSegment. type StartSegmentInputs struct { HTTPRequest *http.Request ServiceName string Operation string Region string Params interface{} } // StartSegment starts a segment of either type DatastoreSegment or // ExternalSegment given the serviceName provided. The segment is then added to // the request context. func StartSegment(input StartSegmentInputs) *http.Request { httpCtx := input.HTTPRequest.Context() txn := newrelic.FromContext(httpCtx) var segment endable // Service name capitalization is different for v1 and v2. if input.ServiceName == "dynamodb" || input.ServiceName == "DynamoDB" || input.ServiceName == "dax" { segment = &newrelic.DatastoreSegment{ Product: newrelic.DatastoreDynamoDB, Collection: getTableName(input.Params), Operation: input.Operation, ParameterizedQuery: "", QueryParameters: nil, Host: input.HTTPRequest.URL.Host, PortPathOrID: input.HTTPRequest.URL.Port(), DatabaseName: "", StartTime: txn.StartSegmentNow(), } } else { // Do NOT set any distributed trace headers. // Doing so can cause the AWS SDK's request signature to be invalid on retries. segment = &newrelic.ExternalSegment{ Request: input.HTTPRequest, StartTime: txn.StartSegmentNow(), } } integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, input.Operation) integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSRegion, input.Region) ctx := context.WithValue(httpCtx, segmentContextKey, segment) return input.HTTPRequest.WithContext(ctx) } // EndSegment will end any segment found in the given context. func EndSegment(ctx context.Context, resp *http.Response) { if segment, ok := ctx.Value(segmentContextKey).(endable); ok { if resp != nil { if extSegment, ok := segment.(*newrelic.ExternalSegment); ok { extSegment.Response = resp } if requestID := GetRequestID(resp.Header); requestID != "" { txn := newrelic.FromContext(ctx) integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSRequestID, requestID) } } segment.End() } } go-agent-3.42.0/v3/internal/awssupport/awssupport_test.go000066400000000000000000000044021510742411500235110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.8 // +build go1.8 package awssupport import ( "net/http" "strings" "testing" ) func TestGetTableName(t *testing.T) { str := "this is a string" var emptyStr string strPtr := &str emptyStrPtr := &emptyStr testcases := []struct { params interface{} expected string }{ {params: nil, expected: ""}, {params: str, expected: ""}, {params: strPtr, expected: ""}, {params: struct{ other string }{other: str}, expected: ""}, {params: &struct{ other string }{other: str}, expected: ""}, {params: struct{ TableName bool }{TableName: true}, expected: ""}, {params: &struct{ TableName bool }{TableName: true}, expected: ""}, {params: struct{ TableName string }{TableName: str}, expected: ""}, {params: &struct{ TableName string }{TableName: str}, expected: ""}, {params: struct{ TableName *string }{TableName: nil}, expected: ""}, {params: &struct{ TableName *string }{TableName: nil}, expected: ""}, {params: struct{ TableName *string }{TableName: emptyStrPtr}, expected: ""}, {params: &struct{ TableName *string }{TableName: emptyStrPtr}, expected: ""}, {params: struct{ TableName *string }{TableName: strPtr}, expected: ""}, {params: &struct{ TableName *string }{TableName: strPtr}, expected: str}, } for i, test := range testcases { if out := getTableName(test.params); test.expected != out { t.Error(i, out, test.params, test.expected) } } } func TestGetRequestID(t *testing.T) { primary := "X-Amzn-Requestid" secondary := "X-Amz-Request-Id" testcases := []struct { hdr http.Header expected string }{ {hdr: http.Header{ "hello": []string{"world"}, }, expected: ""}, {hdr: http.Header{ strings.ToUpper(primary): []string{"hello"}, }, expected: ""}, {hdr: http.Header{ primary: []string{"hello"}, }, expected: "hello"}, {hdr: http.Header{ secondary: []string{"hello"}, }, expected: "hello"}, {hdr: http.Header{ primary: []string{"hello"}, secondary: []string{"world"}, }, expected: "hello"}, {hdr: http.Header{}, expected: ""}, } for i, test := range testcases { if out := GetRequestID(test.hdr); test.expected != out { t.Error(i, out, test.hdr, test.expected) } } } go-agent-3.42.0/v3/internal/cat/000077500000000000000000000000001510742411500162145ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/cat/appdata.go000066400000000000000000000062671510742411500201700ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "bytes" "encoding/json" "errors" "github.com/newrelic/go-agent/v3/internal/jsonx" ) // AppDataHeader represents a decoded AppData header. type AppDataHeader struct { CrossProcessID string TransactionName string QueueTimeInSeconds float64 ResponseTimeInSeconds float64 ContentLength int64 TransactionGUID string } var ( errInvalidAppDataJSON = errors.New("invalid transaction data JSON") errInvalidAppDataCrossProcessID = errors.New("cross process ID is not a string") errInvalidAppDataTransactionName = errors.New("transaction name is not a string") errInvalidAppDataQueueTimeInSeconds = errors.New("queue time is not a float64") errInvalidAppDataResponseTimeInSeconds = errors.New("response time is not a float64") errInvalidAppDataContentLength = errors.New("content length is not a float64") errInvalidAppDataTransactionGUID = errors.New("transaction GUID is not a string") ) // MarshalJSON marshalls an AppDataHeader as raw JSON. func (appData *AppDataHeader) MarshalJSON() ([]byte, error) { buf := bytes.NewBufferString("[") jsonx.AppendString(buf, appData.CrossProcessID) buf.WriteString(",") jsonx.AppendString(buf, appData.TransactionName) buf.WriteString(",") jsonx.AppendFloat(buf, appData.QueueTimeInSeconds) buf.WriteString(",") jsonx.AppendFloat(buf, appData.ResponseTimeInSeconds) buf.WriteString(",") jsonx.AppendInt(buf, appData.ContentLength) buf.WriteString(",") jsonx.AppendString(buf, appData.TransactionGUID) // The mysterious unused field. We don't need to round trip this, so we'll // just hardcode it to false. buf.WriteString(",false]") return buf.Bytes(), nil } // UnmarshalJSON unmarshalls an AppDataHeader from raw JSON. func (appData *AppDataHeader) UnmarshalJSON(data []byte) error { var ok bool var v interface{} if err := json.Unmarshal(data, &v); err != nil { return err } arr, ok := v.([]interface{}) if !ok { return errInvalidAppDataJSON } if len(arr) < 7 { return errUnexpectedArraySize{ label: "unexpected number of application data elements", expected: 7, actual: len(arr), } } if appData.CrossProcessID, ok = arr[0].(string); !ok { return errInvalidAppDataCrossProcessID } if appData.TransactionName, ok = arr[1].(string); !ok { return errInvalidAppDataTransactionName } if appData.QueueTimeInSeconds, ok = arr[2].(float64); !ok { return errInvalidAppDataQueueTimeInSeconds } if appData.ResponseTimeInSeconds, ok = arr[3].(float64); !ok { return errInvalidAppDataResponseTimeInSeconds } cl, ok := arr[4].(float64) if !ok { return errInvalidAppDataContentLength } // Content length is specced as int32, but not all agents are consistent on // this in practice. Let's handle it as int64 to maximise compatibility. appData.ContentLength = int64(cl) if appData.TransactionGUID, ok = arr[5].(string); !ok { return errInvalidAppDataTransactionGUID } // As above, we don't bother decoding the unused field here. It just has to // be present (which was checked earlier with the length check). return nil } go-agent-3.42.0/v3/internal/cat/appdata_test.go000066400000000000000000000101231510742411500212110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "encoding/json" "testing" ) func TestAppDataRoundTrip(t *testing.T) { for _, test := range []struct { json string appData AppDataHeader }{ { json: `["xpid","txn",1,2,4096,"guid",false]`, appData: AppDataHeader{ CrossProcessID: "xpid", TransactionName: "txn", QueueTimeInSeconds: 1.0, ResponseTimeInSeconds: 2.0, ContentLength: 4096, TransactionGUID: "guid", }, }, } { // Test unmarshalling. appData := &AppDataHeader{} if err := json.Unmarshal([]byte(test.json), appData); err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.json, err) } if test.appData.CrossProcessID != appData.CrossProcessID { t.Errorf("given %s: CrossProcessID expected to be %s; got %s", test.json, test.appData.CrossProcessID, appData.CrossProcessID) } if test.appData.TransactionName != appData.TransactionName { t.Errorf("given %s: TransactionName expected to be %s; got %s", test.json, test.appData.TransactionName, appData.TransactionName) } if test.appData.QueueTimeInSeconds != appData.QueueTimeInSeconds { t.Errorf("given %s: QueueTimeInSeconds expected to be %f; got %f", test.json, test.appData.QueueTimeInSeconds, appData.QueueTimeInSeconds) } if test.appData.ResponseTimeInSeconds != appData.ResponseTimeInSeconds { t.Errorf("given %s: ResponseTimeInSeconds expected to be %f; got %f", test.json, test.appData.ResponseTimeInSeconds, appData.ResponseTimeInSeconds) } if test.appData.ContentLength != appData.ContentLength { t.Errorf("given %s: ContentLength expected to be %d; got %d", test.json, test.appData.ContentLength, appData.ContentLength) } if test.appData.TransactionGUID != appData.TransactionGUID { t.Errorf("given %s: TransactionGUID expected to be %s; got %s", test.json, test.appData.TransactionGUID, appData.TransactionGUID) } // Test marshalling. data, err := json.Marshal(&test.appData) if err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.json, err) } if string(data) != test.json { t.Errorf("given %s: unexpected JSON %s", test.json, string(data)) } } } func TestAppDataUnmarshal(t *testing.T) { // Test error cases where we get a generic error from the JSON package. for _, input := range []string{ // Basic malformed JSON test: beyond this, we're not going to unit test the // Go standard library's JSON package. ``, } { appData := &AppDataHeader{} if err := json.Unmarshal([]byte(input), appData); err == nil { t.Errorf("given %s: error expected to be non-nil; got nil", input) } } // Test error cases where a specific variable is returned. for _, tc := range []struct { input string err error }{ // Unexpected JSON types. {`false`, errInvalidAppDataJSON}, {`true`, errInvalidAppDataJSON}, {`1234`, errInvalidAppDataJSON}, {`{}`, errInvalidAppDataJSON}, {`""`, errInvalidAppDataJSON}, // Invalid data types for each field in turn. {`[0,"txn",1.0,2.0,4096,"guid",false]`, errInvalidAppDataCrossProcessID}, {`["xpid",0,1.0,2.0,4096,"guid",false]`, errInvalidAppDataTransactionName}, {`["xpid","txn","queue",2.0,4096,"guid",false]`, errInvalidAppDataQueueTimeInSeconds}, {`["xpid","txn",1.0,"response",4096,"guid",false]`, errInvalidAppDataResponseTimeInSeconds}, {`["xpid","txn",1.0,2.0,"content length","guid",false]`, errInvalidAppDataContentLength}, {`["xpid","txn",1.0,2.0,4096,0,false]`, errInvalidAppDataTransactionGUID}, } { appData := &AppDataHeader{} if err := json.Unmarshal([]byte(tc.input), appData); err != tc.err { t.Errorf("given %s: error expected to be %v; got %v", tc.input, tc.err, err) } } // Test error cases where the incorrect number of elements was provided. for _, input := range []string{ `[]`, `[1,2,3,4,5,6]`, } { appData := &AppDataHeader{} err := json.Unmarshal([]byte(input), appData) if _, ok := err.(errUnexpectedArraySize); !ok { t.Errorf("given %s: error expected to be errUnexpectedArraySize; got %v", input, err) } } } go-agent-3.42.0/v3/internal/cat/errors.go000066400000000000000000000005341510742411500200610ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "fmt" ) type errUnexpectedArraySize struct { label string expected int actual int } func (e errUnexpectedArraySize) Error() string { return fmt.Sprintf("%s: expected %d; got %d", e.label, e.expected, e.actual) } go-agent-3.42.0/v3/internal/cat/headers.go000066400000000000000000000012221510742411500201530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package cat provides functionality related to the wire format of CAT // headers. package cat // These header names don't match the spec in terms of their casing, but does // match what Go will give us from http.CanonicalHeaderKey(). Besides, HTTP // headers are case insensitive anyway. Rejoice! const ( NewRelicIDName = "X-Newrelic-Id" NewRelicTxnName = "X-Newrelic-Transaction" NewRelicAppDataName = "X-Newrelic-App-Data" NewRelicSyntheticsName = "X-Newrelic-Synthetics" NewRelicSyntheticsInfo = "X-Newrelic-Synthetics-Info" ) go-agent-3.42.0/v3/internal/cat/id.go000066400000000000000000000016201510742411500171360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "errors" "strconv" "strings" ) // IDHeader represents a decoded cross process ID header (generally encoded as // a string in the form ACCOUNT#BLOB). type IDHeader struct { AccountID int Blob string } var ( errInvalidAccountID = errors.New("invalid account ID") ) // NewIDHeader parses the given decoded ID header and creates an IDHeader // representing it. func NewIDHeader(in []byte) (*IDHeader, error) { parts := strings.Split(string(in), "#") if len(parts) != 2 { return nil, errUnexpectedArraySize{ label: "unexpected number of ID elements", expected: 2, actual: len(parts), } } account, err := strconv.Atoi(parts[0]) if err != nil { return nil, errInvalidAccountID } return &IDHeader{ AccountID: account, Blob: parts[1], }, nil } go-agent-3.42.0/v3/internal/cat/id_test.go000066400000000000000000000027551510742411500202070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "testing" ) func TestIDHeaderUnmarshal(t *testing.T) { // Test error cases where the output is errUnexpectedArraySize. for _, input := range []string{ ``, `1234`, `1234#5678#90`, `foo`, } { _, err := NewIDHeader([]byte(input)) if _, ok := err.(errUnexpectedArraySize); !ok { t.Errorf("given %s: error expected to be errUnexpectedArraySize; got %v", input, err) } } // Test error cases where the output is errInvalidAccountID. for _, input := range []string{ `#1234`, `foo#bar`, } { if _, err := NewIDHeader([]byte(input)); err != errInvalidAccountID { t.Errorf("given %s: error expected to be %v; got %v", input, errInvalidAccountID, err) } } // Test success cases. for _, test := range []struct { input string expected IDHeader }{ {`1234#`, IDHeader{1234, ""}}, {`1234#5678`, IDHeader{1234, "5678"}}, {`1234#blob`, IDHeader{1234, "blob"}}, {`0#5678`, IDHeader{0, "5678"}}, } { id, err := NewIDHeader([]byte(test.input)) if err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.input, err) } if test.expected.AccountID != id.AccountID { t.Errorf("given %s: account ID expected to be %d; got %d", test.input, test.expected.AccountID, id.AccountID) } if test.expected.Blob != id.Blob { t.Errorf("given %s: account ID expected to be %s; got %s", test.input, test.expected.Blob, id.Blob) } } } go-agent-3.42.0/v3/internal/cat/path_hash.go000066400000000000000000000020431510742411500205010ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "crypto/md5" "encoding/binary" "fmt" "regexp" ) var pathHashValidator = regexp.MustCompile("^[0-9a-f]{8}$") // GeneratePathHash generates a path hash given a referring path hash, // transaction name, and application name. referringPathHash can be an empty // string if there was no referring path hash. func GeneratePathHash(referringPathHash, txnName, appName string) (string, error) { var rph uint32 if referringPathHash != "" { if !pathHashValidator.MatchString(referringPathHash) { // Per the spec, invalid referring path hashes should be treated as "0". referringPathHash = "0" } if _, err := fmt.Sscanf(referringPathHash, "%x", &rph); err != nil { fmt.Println(rph) return "", err } rph = (rph << 1) | (rph >> 31) } hashInput := fmt.Sprintf("%s;%s", appName, txnName) hash := md5.Sum([]byte(hashInput)) low32 := binary.BigEndian.Uint32(hash[12:]) return fmt.Sprintf("%08x", rph^low32), nil } go-agent-3.42.0/v3/internal/cat/path_hash_test.go000066400000000000000000000014611510742411500215430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestGeneratePathHash(t *testing.T) { var tcs []struct { Name string ReferringPathHash string ApplicationName string TransactionName string ExpectedPathHash string } err := crossagent.ReadJSON("cat/path_hashing.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { hash, err := GeneratePathHash(tc.ReferringPathHash, tc.TransactionName, tc.ApplicationName) if err != nil { t.Errorf("%s: error expected to be nil; got %v", tc.Name, err) } if hash != tc.ExpectedPathHash { t.Errorf("%s: expected %s; got %s", tc.Name, tc.ExpectedPathHash, hash) } } } go-agent-3.42.0/v3/internal/cat/synthetics.go000066400000000000000000000107301510742411500207410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "encoding/json" "errors" "fmt" ) // SyntheticsHeader represents a decoded Synthetics header. type SyntheticsHeader struct { Version int AccountID int ResourceID string JobID string MonitorID string } // SyntheticsInfo represents a decoded synthetics info payload. type SyntheticsInfo struct { Version int Type string Initiator string Attributes map[string]string } var ( errInvalidSyntheticsJSON = errors.New("invalid synthetics JSON") errInvalidSyntheticsInfoJSON = errors.New("invalid synthetics info JSON") errInvalidSyntheticsVersion = errors.New("version is not a float64") errInvalidSyntheticsAccountID = errors.New("account ID is not a float64") errInvalidSyntheticsResourceID = errors.New("synthetics resource ID is not a string") errInvalidSyntheticsJobID = errors.New("synthetics job ID is not a string") errInvalidSyntheticsMonitorID = errors.New("synthetics monitor ID is not a string") errInvalidSyntheticsInfoVersion = errors.New("synthetics info version is not a float64") errMissingSyntheticsInfoVersion = errors.New("synthetics info version is missing from JSON object") errInvalidSyntheticsInfoType = errors.New("synthetics info type is not a string") errMissingSyntheticsInfoType = errors.New("synthetics info type is missing from JSON object") errInvalidSyntheticsInfoInitiator = errors.New("synthetics info initiator is not a string") errMissingSyntheticsInfoInitiator = errors.New("synthetics info initiator is missing from JSON object") errInvalidSyntheticsInfoAttributes = errors.New("synthetics info attributes is not a map") errInvalidSyntheticsInfoAttributeVal = errors.New("synthetics info keys and values must be strings") ) type errUnexpectedSyntheticsVersion int func (e errUnexpectedSyntheticsVersion) Error() string { return fmt.Sprintf("unexpected synthetics header version: %d", e) } // UnmarshalJSON unmarshalls a SyntheticsHeader from raw JSON. func (s *SyntheticsHeader) UnmarshalJSON(data []byte) error { var ok bool var v interface{} if err := json.Unmarshal(data, &v); err != nil { return err } arr, ok := v.([]interface{}) if !ok { return errInvalidSyntheticsJSON } if len(arr) != 5 { return errUnexpectedArraySize{ label: "unexpected number of application data elements", expected: 5, actual: len(arr), } } version, ok := arr[0].(float64) if !ok { return errInvalidSyntheticsVersion } s.Version = int(version) if s.Version != 1 { return errUnexpectedSyntheticsVersion(s.Version) } accountID, ok := arr[1].(float64) if !ok { return errInvalidSyntheticsAccountID } s.AccountID = int(accountID) if s.ResourceID, ok = arr[2].(string); !ok { return errInvalidSyntheticsResourceID } if s.JobID, ok = arr[3].(string); !ok { return errInvalidSyntheticsJobID } if s.MonitorID, ok = arr[4].(string); !ok { return errInvalidSyntheticsMonitorID } return nil } const ( versionKey = "version" typeKey = "type" initiatorKey = "initiator" attributesKey = "attributes" ) // UnmarshalJSON unmarshalls a SyntheticsInfo from raw JSON. func (s *SyntheticsInfo) UnmarshalJSON(data []byte) error { var v any if err := json.Unmarshal(data, &v); err != nil { return err } m, ok := v.(map[string]any) if !ok { return errInvalidSyntheticsInfoJSON } version, ok := m[versionKey] if !ok { return errMissingSyntheticsInfoVersion } versionFloat, ok := version.(float64) if !ok { return errInvalidSyntheticsInfoVersion } s.Version = int(versionFloat) if s.Version != 1 { return errUnexpectedSyntheticsVersion(s.Version) } infoType, ok := m[typeKey] if !ok { return errMissingSyntheticsInfoType } s.Type, ok = infoType.(string) if !ok { return errInvalidSyntheticsInfoType } initiator, ok := m[initiatorKey] if !ok { return errMissingSyntheticsInfoInitiator } s.Initiator, ok = initiator.(string) if !ok { return errInvalidSyntheticsInfoInitiator } attrs, ok := m[attributesKey] if ok { attrMap, ok := attrs.(map[string]any) if !ok { return errInvalidSyntheticsInfoAttributes } for k, v := range attrMap { val, ok := v.(string) if !ok { return errInvalidSyntheticsInfoAttributeVal } if s.Attributes == nil { s.Attributes = map[string]string{k: val} } else { s.Attributes[k] = val } } } return nil } go-agent-3.42.0/v3/internal/cat/synthetics_test.go000066400000000000000000000175271510742411500220130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "encoding/json" "fmt" "testing" ) func TestSyntheticsUnmarshalInvalid(t *testing.T) { // Test error cases where we get a generic error from the JSON package. for _, input := range []string{ // Basic malformed JSON test: beyond this, we're not going to unit test the // Go standard library's JSON package. ``, } { synthetics := &SyntheticsHeader{} if err := json.Unmarshal([]byte(input), synthetics); err == nil { t.Errorf("given %s: error expected to be non-nil; got nil", input) } } // Test error cases where the incorrect number of elements was provided. for _, input := range []string{ `[]`, `[1,2,3,4]`, } { synthetics := &SyntheticsHeader{} err := json.Unmarshal([]byte(input), synthetics) if _, ok := err.(errUnexpectedArraySize); !ok { t.Errorf("given %s: error expected to be errUnexpectedArraySize; got %v", input, err) } } // Test error cases with invalid version numbers. for _, input := range []string{ `[0,1234,"resource","job","monitor"]`, `[2,1234,"resource","job","monitor"]`, } { synthetics := &SyntheticsHeader{} err := json.Unmarshal([]byte(input), synthetics) if _, ok := err.(errUnexpectedSyntheticsVersion); !ok { t.Errorf("given %s: error expected to be errUnexpectedSyntheticsVersion; got %v", input, err) } } // Test error cases where a specific variable is returned. for _, tc := range []struct { input string err error }{ // Unexpected JSON types. {`false`, errInvalidSyntheticsJSON}, {`true`, errInvalidSyntheticsJSON}, {`1234`, errInvalidSyntheticsJSON}, {`{}`, errInvalidSyntheticsJSON}, {`""`, errInvalidSyntheticsJSON}, // Invalid data types for each field in turn. {`["version",1234,"resource","job","monitor"]`, errInvalidSyntheticsVersion}, {`[1,"account","resource","job","monitor"]`, errInvalidSyntheticsAccountID}, {`[1,1234,0,"job","monitor"]`, errInvalidSyntheticsResourceID}, {`[1,1234,"resource",-1,"monitor"]`, errInvalidSyntheticsJobID}, {`[1,1234,"resource","job",false]`, errInvalidSyntheticsMonitorID}, } { synthetics := &SyntheticsHeader{} if err := json.Unmarshal([]byte(tc.input), synthetics); err != tc.err { t.Errorf("given %s: error expected to be %v; got %v", tc.input, tc.err, err) } } } func TestSyntheticsUnmarshalValid(t *testing.T) { for _, test := range []struct { json string synthetics SyntheticsHeader }{ { json: `[1,1234,"resource","job","monitor"]`, synthetics: SyntheticsHeader{ Version: 1, AccountID: 1234, ResourceID: "resource", JobID: "job", MonitorID: "monitor", }, }, } { // Test unmarshalling. synthetics := &SyntheticsHeader{} if err := json.Unmarshal([]byte(test.json), synthetics); err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.json, err) } if test.synthetics.Version != synthetics.Version { t.Errorf("given %s: Version expected to be %d; got %d", test.json, test.synthetics.Version, synthetics.Version) } if test.synthetics.AccountID != synthetics.AccountID { t.Errorf("given %s: AccountID expected to be %d; got %d", test.json, test.synthetics.AccountID, synthetics.AccountID) } if test.synthetics.ResourceID != synthetics.ResourceID { t.Errorf("given %s: ResourceID expected to be %s; got %s", test.json, test.synthetics.ResourceID, synthetics.ResourceID) } if test.synthetics.JobID != synthetics.JobID { t.Errorf("given %s: JobID expected to be %s; got %s", test.json, test.synthetics.JobID, synthetics.JobID) } if test.synthetics.MonitorID != synthetics.MonitorID { t.Errorf("given %s: MonitorID expected to be %s; got %s", test.json, test.synthetics.MonitorID, synthetics.MonitorID) } } } func TestSyntheticsInfoUnmarshal(t *testing.T) { type testCase struct { name string json string syntheticsInfo SyntheticsInfo expectedError error } testCases := []testCase{ { name: "missing type field", json: `{"version":1,"initiator":"cli"}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errMissingSyntheticsInfoType, }, { name: "invalid type field", json: `{"version":1,"initiator":"cli","type":1}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errInvalidSyntheticsInfoType, }, { name: "missing initiator field", json: `{"version":1,"type":"scheduled"}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errMissingSyntheticsInfoInitiator, }, { name: "invalid initiator field", json: `{"version":1,"initiator":1,"type":"scheduled"}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errInvalidSyntheticsInfoInitiator, }, { name: "missing version field", json: `{"type":"scheduled"}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errMissingSyntheticsInfoVersion, }, { name: "invalid version field", json: `{"version":"1","initiator":"cli","type":"scheduled"}`, syntheticsInfo: SyntheticsInfo{}, expectedError: errInvalidSyntheticsInfoVersion, }, { name: "valid synthetics info", json: `{"version":1,"type":"scheduled","initiator":"cli"}`, syntheticsInfo: SyntheticsInfo{ Version: 1, Type: "scheduled", Initiator: "cli", }, expectedError: nil, }, { name: "valid synthetics info with attributes", json: `{"version":1,"type":"scheduled","initiator":"cli","attributes":{"hi":"hello"}}`, syntheticsInfo: SyntheticsInfo{ Version: 1, Type: "scheduled", Initiator: "cli", Attributes: map[string]string{"hi": "hello"}, }, expectedError: nil, }, { name: "valid synthetics info with invalid attributes", json: `{"version":1,"type":"scheduled","initiator":"cli","attributes":{"hi":1}}`, syntheticsInfo: SyntheticsInfo{ Version: 1, Type: "scheduled", Initiator: "cli", Attributes: nil, }, expectedError: errInvalidSyntheticsInfoAttributeVal, }, } for _, testCase := range testCases { syntheticsInfo := SyntheticsInfo{} err := syntheticsInfo.UnmarshalJSON([]byte(testCase.json)) if testCase.expectedError == nil { if err != nil { recordError(t, testCase.name, fmt.Sprintf("expected synthetics info to unmarshal without error, but got error: %v", err)) } expect := testCase.syntheticsInfo if expect.Version != syntheticsInfo.Version { recordError(t, testCase.name, fmt.Sprintf(`expected version "%d", but got "%d"`, expect.Version, syntheticsInfo.Version)) } if expect.Type != syntheticsInfo.Type { recordError(t, testCase.name, fmt.Sprintf(`expected version "%s", but got "%s"`, expect.Type, syntheticsInfo.Type)) } if expect.Initiator != syntheticsInfo.Initiator { recordError(t, testCase.name, fmt.Sprintf(`expected version "%s", but got "%s"`, expect.Initiator, syntheticsInfo.Initiator)) } if len(expect.Attributes) != 0 { if len(syntheticsInfo.Attributes) == 0 { recordError(t, testCase.name, fmt.Sprintf(`expected attribute array to have %d elements, but it only had %d`, len(expect.Attributes), len(syntheticsInfo.Attributes))) } for ek, ev := range expect.Attributes { v, ok := syntheticsInfo.Attributes[ek] if !ok { recordError(t, testCase.name, fmt.Sprintf(`expected attributes to contain key "%s", but it did not`, ek)) } if ev != v { recordError(t, testCase.name, fmt.Sprintf(`expected attributes to contain "%s":"%s", but it contained "%s":"%s"`, ek, ev, ek, v)) } } } } else { if err != testCase.expectedError { recordError(t, testCase.name, fmt.Sprintf(`expected synthetics info to unmarshal with error "%v", but got "%v"`, testCase.expectedError, err)) } } } } func recordError(t *testing.T, test, err string) { t.Errorf("%s: %s", test, err) } go-agent-3.42.0/v3/internal/cat/txndata.go000066400000000000000000000044161510742411500202130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "bytes" "encoding/json" "errors" "github.com/newrelic/go-agent/v3/internal/jsonx" ) // TxnDataHeader represents a decoded TxnData header. type TxnDataHeader struct { GUID string TripID string PathHash string } var ( errInvalidTxnDataJSON = errors.New("invalid transaction data JSON") errInvalidTxnDataGUID = errors.New("GUID is not a string") errInvalidTxnDataTripID = errors.New("trip ID is not a string or null") errInvalidTxnDataPathHash = errors.New("path hash is not a string or null") ) // MarshalJSON marshalls a TxnDataHeader as raw JSON. func (txnData *TxnDataHeader) MarshalJSON() ([]byte, error) { // Note that, although there are two and four element versions of this header // in the wild, we will only ever generate the four element version. buf := bytes.NewBufferString("[") jsonx.AppendString(buf, txnData.GUID) // Write the unused second field. buf.WriteString(",false,") jsonx.AppendString(buf, txnData.TripID) buf.WriteString(",") jsonx.AppendString(buf, txnData.PathHash) buf.WriteString("]") return buf.Bytes(), nil } // UnmarshalJSON unmarshalls a TxnDataHeader from raw JSON. func (txnData *TxnDataHeader) UnmarshalJSON(data []byte) error { var ok bool var v interface{} if err := json.Unmarshal(data, &v); err != nil { return err } arr, ok := v.([]interface{}) if !ok { return errInvalidTxnDataJSON } if len(arr) < 2 { return errUnexpectedArraySize{ label: "unexpected number of transaction data elements", expected: 2, actual: len(arr), } } if txnData.GUID, ok = arr[0].(string); !ok { return errInvalidTxnDataGUID } // Ignore the unused second field. // Set up defaults for the optional values. txnData.TripID = "" txnData.PathHash = "" if len(arr) >= 3 { // Per the cross agent tests, an explicit null is valid here. if nil != arr[2] { if txnData.TripID, ok = arr[2].(string); !ok { return errInvalidTxnDataTripID } } if len(arr) >= 4 { // Per the cross agent tests, an explicit null is also valid here. if nil != arr[3] { if txnData.PathHash, ok = arr[3].(string); !ok { return errInvalidTxnDataPathHash } } } } return nil } go-agent-3.42.0/v3/internal/cat/txndata_test.go000066400000000000000000000071551510742411500212550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package cat import ( "encoding/json" "testing" ) func TestTxnDataRoundTrip(t *testing.T) { for _, test := range []struct { input string output string txnData TxnDataHeader }{ { input: `["guid",false]`, output: `["guid",false,"",""]`, txnData: TxnDataHeader{ GUID: "guid", TripID: "", PathHash: "", }, }, { input: `["guid",false,"trip"]`, output: `["guid",false,"trip",""]`, txnData: TxnDataHeader{ GUID: "guid", TripID: "trip", PathHash: "", }, }, { input: `["guid",false,null]`, output: `["guid",false,"",""]`, txnData: TxnDataHeader{ GUID: "guid", TripID: "", PathHash: "", }, }, { input: `["guid",false,"trip",null]`, output: `["guid",false,"trip",""]`, txnData: TxnDataHeader{ GUID: "guid", TripID: "trip", PathHash: "", }, }, { input: `["guid",false,"trip","hash"]`, output: `["guid",false,"trip","hash"]`, txnData: TxnDataHeader{ GUID: "guid", TripID: "trip", PathHash: "hash", }, }, } { // Test unmarshalling. txnData := &TxnDataHeader{} if err := json.Unmarshal([]byte(test.input), txnData); err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.input, err) } if test.txnData.GUID != txnData.GUID { t.Errorf("given %s: GUID expected to be %s; got %s", test.input, test.txnData.GUID, txnData.GUID) } if test.txnData.TripID != txnData.TripID { t.Errorf("given %s: TripID expected to be %s; got %s", test.input, test.txnData.TripID, txnData.TripID) } if test.txnData.PathHash != txnData.PathHash { t.Errorf("given %s: PathHash expected to be %s; got %s", test.input, test.txnData.PathHash, txnData.PathHash) } // Test marshalling. data, err := json.Marshal(&test.txnData) if err != nil { t.Errorf("given %s: error expected to be nil; got %v", test.output, err) } if string(data) != test.output { t.Errorf("given %s: unexpected JSON %s", test.output, string(data)) } } } func TestTxnDataUnmarshal(t *testing.T) { // Test error cases where we get a generic error from the JSON package. for _, input := range []string{ // Basic malformed JSON test: beyond this, we're not going to unit test the // Go standard library's JSON package. ``, } { txnData := &TxnDataHeader{} if err := json.Unmarshal([]byte(input), txnData); err == nil { t.Errorf("given %s: error expected to be non-nil; got nil", input) } } // Test error cases where the incorrect number of elements was provided. for _, input := range []string{ `[]`, `[1]`, } { txnData := &TxnDataHeader{} err := json.Unmarshal([]byte(input), txnData) if _, ok := err.(errUnexpectedArraySize); !ok { t.Errorf("given %s: error expected to be errUnexpectedArraySize; got %v", input, err) } } // Test error cases where a specific variable is returned. for _, tc := range []struct { input string err error }{ // Unexpected JSON types. {`false`, errInvalidTxnDataJSON}, {`true`, errInvalidTxnDataJSON}, {`1234`, errInvalidTxnDataJSON}, {`{}`, errInvalidTxnDataJSON}, {`""`, errInvalidTxnDataJSON}, // Invalid data types for each field in turn. {`[false,false,"trip","hash"]`, errInvalidTxnDataGUID}, {`["guid",false,0,"hash"]`, errInvalidTxnDataTripID}, {`["guid",false,"trip",[]]`, errInvalidTxnDataPathHash}, } { txnData := &TxnDataHeader{} if err := json.Unmarshal([]byte(tc.input), txnData); err != tc.err { t.Errorf("given %s: error expected to be %v; got %v", tc.input, tc.err, err) } } } go-agent-3.42.0/v3/internal/com_newrelic_trace_v1/000077500000000000000000000000001510742411500216775ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/com_newrelic_trace_v1/README.md000066400000000000000000000014321510742411500231560ustar00rootroot00000000000000# com_newrelic_trace_v1 To generate the `v1.pb.go` code, run the following from the top level `github.com/newrelic/go-agent` package: ``` protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ v3/internal/com_newrelic_trace_v1/v1.proto ``` Be mindful which version of `protoc-gen-go` and `protoc-gen-go-grpc` you are using. Upgrade both of these tools to the latest with: ``` go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` ## When you regenerate the file Once you have generated the code, you will need to add a build tag to the file: ```go // +build go1.9 ``` This is because the gRPC/Protocol Buffer libraries only support Go 1.9 and above. go-agent-3.42.0/v3/internal/com_newrelic_trace_v1/v1.pb.go000066400000000000000000000511401510742411500231550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 // protoc v5.27.3 // source: v3/internal/com_newrelic_trace_v1/v1.proto package com_newrelic_trace_v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type SpanBatch struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Spans []*Span `protobuf:"bytes,1,rep,name=spans,proto3" json:"spans,omitempty"` } func (x *SpanBatch) Reset() { *x = SpanBatch{} if protoimpl.UnsafeEnabled { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SpanBatch) String() string { return protoimpl.X.MessageStringOf(x) } func (*SpanBatch) ProtoMessage() {} func (x *SpanBatch) ProtoReflect() protoreflect.Message { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SpanBatch.ProtoReflect.Descriptor instead. func (*SpanBatch) Descriptor() ([]byte, []int) { return file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescGZIP(), []int{0} } func (x *SpanBatch) GetSpans() []*Span { if x != nil { return x.Spans } return nil } type Span struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields TraceId string `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` Intrinsics map[string]*AttributeValue `protobuf:"bytes,2,rep,name=intrinsics,proto3" json:"intrinsics,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` UserAttributes map[string]*AttributeValue `protobuf:"bytes,3,rep,name=user_attributes,json=userAttributes,proto3" json:"user_attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` AgentAttributes map[string]*AttributeValue `protobuf:"bytes,4,rep,name=agent_attributes,json=agentAttributes,proto3" json:"agent_attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *Span) Reset() { *x = Span{} if protoimpl.UnsafeEnabled { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Span) String() string { return protoimpl.X.MessageStringOf(x) } func (*Span) ProtoMessage() {} func (x *Span) ProtoReflect() protoreflect.Message { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Span.ProtoReflect.Descriptor instead. func (*Span) Descriptor() ([]byte, []int) { return file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescGZIP(), []int{1} } func (x *Span) GetTraceId() string { if x != nil { return x.TraceId } return "" } func (x *Span) GetIntrinsics() map[string]*AttributeValue { if x != nil { return x.Intrinsics } return nil } func (x *Span) GetUserAttributes() map[string]*AttributeValue { if x != nil { return x.UserAttributes } return nil } func (x *Span) GetAgentAttributes() map[string]*AttributeValue { if x != nil { return x.AgentAttributes } return nil } type AttributeValue struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to Value: // *AttributeValue_StringValue // *AttributeValue_BoolValue // *AttributeValue_IntValue // *AttributeValue_DoubleValue Value isAttributeValue_Value `protobuf_oneof:"value"` } func (x *AttributeValue) Reset() { *x = AttributeValue{} if protoimpl.UnsafeEnabled { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AttributeValue) String() string { return protoimpl.X.MessageStringOf(x) } func (*AttributeValue) ProtoMessage() {} func (x *AttributeValue) ProtoReflect() protoreflect.Message { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AttributeValue.ProtoReflect.Descriptor instead. func (*AttributeValue) Descriptor() ([]byte, []int) { return file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescGZIP(), []int{2} } func (m *AttributeValue) GetValue() isAttributeValue_Value { if m != nil { return m.Value } return nil } func (x *AttributeValue) GetStringValue() string { if x, ok := x.GetValue().(*AttributeValue_StringValue); ok { return x.StringValue } return "" } func (x *AttributeValue) GetBoolValue() bool { if x, ok := x.GetValue().(*AttributeValue_BoolValue); ok { return x.BoolValue } return false } func (x *AttributeValue) GetIntValue() int64 { if x, ok := x.GetValue().(*AttributeValue_IntValue); ok { return x.IntValue } return 0 } func (x *AttributeValue) GetDoubleValue() float64 { if x, ok := x.GetValue().(*AttributeValue_DoubleValue); ok { return x.DoubleValue } return 0 } type isAttributeValue_Value interface { isAttributeValue_Value() } type AttributeValue_StringValue struct { StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` } type AttributeValue_BoolValue struct { BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` } type AttributeValue_IntValue struct { IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` } type AttributeValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,4,opt,name=double_value,json=doubleValue,proto3,oneof"` } func (*AttributeValue_StringValue) isAttributeValue_Value() {} func (*AttributeValue_BoolValue) isAttributeValue_Value() {} func (*AttributeValue_IntValue) isAttributeValue_Value() {} func (*AttributeValue_DoubleValue) isAttributeValue_Value() {} type RecordStatus struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields MessagesSeen uint64 `protobuf:"varint,1,opt,name=messages_seen,json=messagesSeen,proto3" json:"messages_seen,omitempty"` } func (x *RecordStatus) Reset() { *x = RecordStatus{} if protoimpl.UnsafeEnabled { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *RecordStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*RecordStatus) ProtoMessage() {} func (x *RecordStatus) ProtoReflect() protoreflect.Message { mi := &file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RecordStatus.ProtoReflect.Descriptor instead. func (*RecordStatus) Descriptor() ([]byte, []int) { return file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescGZIP(), []int{3} } func (x *RecordStatus) GetMessagesSeen() uint64 { if x != nil { return x.MessagesSeen } return 0 } var File_v3_internal_com_newrelic_trace_v1_v1_proto protoreflect.FileDescriptor var file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDesc = []byte{ 0x0a, 0x2a, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6d, 0x5f, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x76, 0x31, 0x2f, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x3e, 0x0a, 0x09, 0x53, 0x70, 0x61, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x31, 0x0a, 0x05, 0x73, 0x70, 0x61, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x52, 0x05, 0x73, 0x70, 0x61, 0x6e, 0x73, 0x22, 0xe0, 0x04, 0x0a, 0x04, 0x53, 0x70, 0x61, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x72, 0x69, 0x6e, 0x73, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x2e, 0x49, 0x6e, 0x74, 0x72, 0x69, 0x6e, 0x73, 0x69, 0x63, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x72, 0x69, 0x6e, 0x73, 0x69, 0x63, 0x73, 0x12, 0x58, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x5b, 0x0a, 0x10, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x64, 0x0a, 0x0f, 0x49, 0x6e, 0x74, 0x72, 0x69, 0x6e, 0x73, 0x69, 0x63, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x68, 0x0a, 0x13, 0x55, 0x73, 0x65, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x14, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa3, 0x01, 0x0a, 0x0e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x33, 0x0a, 0x0c, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x53, 0x65, 0x65, 0x6e, 0x32, 0xc5, 0x01, 0x0a, 0x0d, 0x49, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x54, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x70, 0x61, 0x6e, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5e, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x70, 0x61, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x70, 0x61, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2e, 0x74, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x2f, 0x67, 0x6f, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6d, 0x5f, 0x6e, 0x65, 0x77, 0x72, 0x65, 0x6c, 0x69, 0x63, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescOnce sync.Once file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescData = file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDesc ) func file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescGZIP() []byte { file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescOnce.Do(func() { file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescData) }) return file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDescData } var file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_v3_internal_com_newrelic_trace_v1_v1_proto_goTypes = []interface{}{ (*SpanBatch)(nil), // 0: com.newrelic.trace.v1.SpanBatch (*Span)(nil), // 1: com.newrelic.trace.v1.Span (*AttributeValue)(nil), // 2: com.newrelic.trace.v1.AttributeValue (*RecordStatus)(nil), // 3: com.newrelic.trace.v1.RecordStatus nil, // 4: com.newrelic.trace.v1.Span.IntrinsicsEntry nil, // 5: com.newrelic.trace.v1.Span.UserAttributesEntry nil, // 6: com.newrelic.trace.v1.Span.AgentAttributesEntry } var file_v3_internal_com_newrelic_trace_v1_v1_proto_depIdxs = []int32{ 1, // 0: com.newrelic.trace.v1.SpanBatch.spans:type_name -> com.newrelic.trace.v1.Span 4, // 1: com.newrelic.trace.v1.Span.intrinsics:type_name -> com.newrelic.trace.v1.Span.IntrinsicsEntry 5, // 2: com.newrelic.trace.v1.Span.user_attributes:type_name -> com.newrelic.trace.v1.Span.UserAttributesEntry 6, // 3: com.newrelic.trace.v1.Span.agent_attributes:type_name -> com.newrelic.trace.v1.Span.AgentAttributesEntry 2, // 4: com.newrelic.trace.v1.Span.IntrinsicsEntry.value:type_name -> com.newrelic.trace.v1.AttributeValue 2, // 5: com.newrelic.trace.v1.Span.UserAttributesEntry.value:type_name -> com.newrelic.trace.v1.AttributeValue 2, // 6: com.newrelic.trace.v1.Span.AgentAttributesEntry.value:type_name -> com.newrelic.trace.v1.AttributeValue 1, // 7: com.newrelic.trace.v1.IngestService.RecordSpan:input_type -> com.newrelic.trace.v1.Span 0, // 8: com.newrelic.trace.v1.IngestService.RecordSpanBatch:input_type -> com.newrelic.trace.v1.SpanBatch 3, // 9: com.newrelic.trace.v1.IngestService.RecordSpan:output_type -> com.newrelic.trace.v1.RecordStatus 3, // 10: com.newrelic.trace.v1.IngestService.RecordSpanBatch:output_type -> com.newrelic.trace.v1.RecordStatus 9, // [9:11] is the sub-list for method output_type 7, // [7:9] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name } func init() { file_v3_internal_com_newrelic_trace_v1_v1_proto_init() } func file_v3_internal_com_newrelic_trace_v1_v1_proto_init() { if File_v3_internal_com_newrelic_trace_v1_v1_proto != nil { return } if !protoimpl.UnsafeEnabled { file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SpanBatch); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Span); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AttributeValue); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordStatus); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes[2].OneofWrappers = []interface{}{ (*AttributeValue_StringValue)(nil), (*AttributeValue_BoolValue)(nil), (*AttributeValue_IntValue)(nil), (*AttributeValue_DoubleValue)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDesc, NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 1, }, GoTypes: file_v3_internal_com_newrelic_trace_v1_v1_proto_goTypes, DependencyIndexes: file_v3_internal_com_newrelic_trace_v1_v1_proto_depIdxs, MessageInfos: file_v3_internal_com_newrelic_trace_v1_v1_proto_msgTypes, }.Build() File_v3_internal_com_newrelic_trace_v1_v1_proto = out.File file_v3_internal_com_newrelic_trace_v1_v1_proto_rawDesc = nil file_v3_internal_com_newrelic_trace_v1_v1_proto_goTypes = nil file_v3_internal_com_newrelic_trace_v1_v1_proto_depIdxs = nil } go-agent-3.42.0/v3/internal/com_newrelic_trace_v1/v1.proto000066400000000000000000000021731510742411500233150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; package com.newrelic.trace.v1; option go_package = "github.com/newrelic/go-agent/v3/internal/com_newrelic_trace_v1"; service IngestService { // Accepts a stream of Span messages, and returns an irregular stream of // RecordStatus messages. rpc RecordSpan(stream Span) returns (stream RecordStatus) {} // Accepts a stream of SpanBatch messages, and returns an irregular // stream of RecordStatus messages. This endpoint can be used to improve // throughput when Span messages are small rpc RecordSpanBatch(stream SpanBatch) returns (stream RecordStatus) {} } message SpanBatch { repeated Span spans = 1; } message Span { string trace_id = 1; map intrinsics = 2; map user_attributes = 3; map agent_attributes = 4; } message AttributeValue { oneof value { string string_value = 1; bool bool_value = 2; int64 int_value = 3; double double_value = 4; } } message RecordStatus { uint64 messages_seen = 1; } go-agent-3.42.0/v3/internal/com_newrelic_trace_v1/v1_grpc.pb.go000066400000000000000000000155751510742411500242040ustar00rootroot00000000000000// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 // - protoc v5.27.3 // source: v3/internal/com_newrelic_trace_v1/v1.proto package com_newrelic_trace_v1 import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // IngestServiceClient is the client API for IngestService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type IngestServiceClient interface { // Accepts a stream of Span messages, and returns an irregular stream of // RecordStatus messages. RecordSpan(ctx context.Context, opts ...grpc.CallOption) (IngestService_RecordSpanClient, error) // Accepts a stream of SpanBatch messages, and returns an irregular // stream of RecordStatus messages. This endpoint can be used to improve // throughput when Span messages are small RecordSpanBatch(ctx context.Context, opts ...grpc.CallOption) (IngestService_RecordSpanBatchClient, error) } type ingestServiceClient struct { cc grpc.ClientConnInterface } func NewIngestServiceClient(cc grpc.ClientConnInterface) IngestServiceClient { return &ingestServiceClient{cc} } func (c *ingestServiceClient) RecordSpan(ctx context.Context, opts ...grpc.CallOption) (IngestService_RecordSpanClient, error) { stream, err := c.cc.NewStream(ctx, &IngestService_ServiceDesc.Streams[0], "/com.newrelic.trace.v1.IngestService/RecordSpan", opts...) if err != nil { return nil, err } x := &ingestServiceRecordSpanClient{stream} return x, nil } type IngestService_RecordSpanClient interface { Send(*Span) error Recv() (*RecordStatus, error) grpc.ClientStream } type ingestServiceRecordSpanClient struct { grpc.ClientStream } func (x *ingestServiceRecordSpanClient) Send(m *Span) error { return x.ClientStream.SendMsg(m) } func (x *ingestServiceRecordSpanClient) Recv() (*RecordStatus, error) { m := new(RecordStatus) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *ingestServiceClient) RecordSpanBatch(ctx context.Context, opts ...grpc.CallOption) (IngestService_RecordSpanBatchClient, error) { stream, err := c.cc.NewStream(ctx, &IngestService_ServiceDesc.Streams[1], "/com.newrelic.trace.v1.IngestService/RecordSpanBatch", opts...) if err != nil { return nil, err } x := &ingestServiceRecordSpanBatchClient{stream} return x, nil } type IngestService_RecordSpanBatchClient interface { Send(*SpanBatch) error Recv() (*RecordStatus, error) grpc.ClientStream } type ingestServiceRecordSpanBatchClient struct { grpc.ClientStream } func (x *ingestServiceRecordSpanBatchClient) Send(m *SpanBatch) error { return x.ClientStream.SendMsg(m) } func (x *ingestServiceRecordSpanBatchClient) Recv() (*RecordStatus, error) { m := new(RecordStatus) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // IngestServiceServer is the server API for IngestService service. // All implementations must embed UnimplementedIngestServiceServer // for forward compatibility type IngestServiceServer interface { // Accepts a stream of Span messages, and returns an irregular stream of // RecordStatus messages. RecordSpan(IngestService_RecordSpanServer) error // Accepts a stream of SpanBatch messages, and returns an irregular // stream of RecordStatus messages. This endpoint can be used to improve // throughput when Span messages are small RecordSpanBatch(IngestService_RecordSpanBatchServer) error mustEmbedUnimplementedIngestServiceServer() } // UnimplementedIngestServiceServer must be embedded to have forward compatible implementations. type UnimplementedIngestServiceServer struct { } func (UnimplementedIngestServiceServer) RecordSpan(IngestService_RecordSpanServer) error { return status.Errorf(codes.Unimplemented, "method RecordSpan not implemented") } func (UnimplementedIngestServiceServer) RecordSpanBatch(IngestService_RecordSpanBatchServer) error { return status.Errorf(codes.Unimplemented, "method RecordSpanBatch not implemented") } func (UnimplementedIngestServiceServer) mustEmbedUnimplementedIngestServiceServer() {} // UnsafeIngestServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to IngestServiceServer will // result in compilation errors. type UnsafeIngestServiceServer interface { mustEmbedUnimplementedIngestServiceServer() } func RegisterIngestServiceServer(s grpc.ServiceRegistrar, srv IngestServiceServer) { s.RegisterService(&IngestService_ServiceDesc, srv) } func _IngestService_RecordSpan_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(IngestServiceServer).RecordSpan(&ingestServiceRecordSpanServer{stream}) } type IngestService_RecordSpanServer interface { Send(*RecordStatus) error Recv() (*Span, error) grpc.ServerStream } type ingestServiceRecordSpanServer struct { grpc.ServerStream } func (x *ingestServiceRecordSpanServer) Send(m *RecordStatus) error { return x.ServerStream.SendMsg(m) } func (x *ingestServiceRecordSpanServer) Recv() (*Span, error) { m := new(Span) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _IngestService_RecordSpanBatch_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(IngestServiceServer).RecordSpanBatch(&ingestServiceRecordSpanBatchServer{stream}) } type IngestService_RecordSpanBatchServer interface { Send(*RecordStatus) error Recv() (*SpanBatch, error) grpc.ServerStream } type ingestServiceRecordSpanBatchServer struct { grpc.ServerStream } func (x *ingestServiceRecordSpanBatchServer) Send(m *RecordStatus) error { return x.ServerStream.SendMsg(m) } func (x *ingestServiceRecordSpanBatchServer) Recv() (*SpanBatch, error) { m := new(SpanBatch) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // IngestService_ServiceDesc is the grpc.ServiceDesc for IngestService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var IngestService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "com.newrelic.trace.v1.IngestService", HandlerType: (*IngestServiceServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "RecordSpan", Handler: _IngestService_RecordSpan_Handler, ServerStreams: true, ClientStreams: true, }, { StreamName: "RecordSpanBatch", Handler: _IngestService_RecordSpanBatch_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "v3/internal/com_newrelic_trace_v1/v1.proto", } go-agent-3.42.0/v3/internal/connect_reply.go000066400000000000000000000270131510742411500206430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "context" "encoding/json" "fmt" "net" "strings" "time" ) // AgentRunID identifies the current connection with the collector. type AgentRunID string // DialerFunc is a shorthand that is used in tests for connecting directly // to a local gRPC server type DialerFunc func(context.Context, string) (net.Conn, error) func (id AgentRunID) String() string { return string(id) } // PreconnectReply contains settings from the preconnect endpoint. type PreconnectReply struct { Collector string `json:"redirect_host"` SecurityPolicies SecurityPolicies `json:"security_policies"` } // ConnectReply contains all of the settings and state send down from the // collector. It should not be modified after creation. type ConnectReply struct { RunID AgentRunID `json:"agent_run_id"` RequestHeadersMap map[string]string `json:"request_headers_map"` MaxPayloadSizeInBytes int `json:"max_payload_size_in_bytes"` EntityGUID string `json:"entity_guid"` // Transaction Name Modifiers SegmentTerms segmentRules `json:"transaction_segment_terms"` TxnNameRules MetricRules `json:"transaction_name_rules"` URLRules MetricRules `json:"url_rules"` MetricRules MetricRules `json:"metric_name_rules"` // Cross Process EncodingKey string `json:"encoding_key"` CrossProcessID string `json:"cross_process_id"` TrustedAccounts TrustedAccountSet `json:"trusted_account_ids"` // Settings KeyTxnApdex map[string]float64 `json:"web_transactions_apdex"` ApdexThresholdSeconds float64 `json:"apdex_t"` CollectAnalyticsEvents bool `json:"collect_analytics_events"` CollectCustomEvents bool `json:"collect_custom_events"` CollectTraces bool `json:"collect_traces"` CollectErrors bool `json:"collect_errors"` CollectErrorEvents bool `json:"collect_error_events"` CollectSpanEvents bool `json:"collect_span_events"` // RUM AgentLoader string `json:"js_agent_loader"` Beacon string `json:"beacon"` BrowserKey string `json:"browser_key"` AppID string `json:"application_id"` ErrorBeacon string `json:"error_beacon"` JSAgentFile string `json:"js_agent_file"` // PreconnectReply fields are not in the connect reply, this embedding // is done to simplify code. PreconnectReply `json:"-"` Messages []struct { Message string `json:"message"` Level string `json:"level"` } `json:"messages"` // TraceIDGenerator creates random IDs for distributed tracing. It // exists here in the connect reply so it can be modified to create // deterministic identifiers in tests. TraceIDGenerator *TraceIDGenerator `json:"-"` // DistributedTraceTimestampGenerator allows tests to fix the outbound // DT header timestamp. DistributedTraceTimestampGenerator func() time.Time `json:"-"` // TraceObsDialer allows tests to connect to a local TraceObserver directly TraceObsDialer DialerFunc // BetterCAT/Distributed Tracing AccountID string `json:"account_id"` TrustedAccountKey string `json:"trusted_account_key"` PrimaryAppID string `json:"primary_application_id"` SamplingTarget uint64 `json:"sampling_target"` SamplingTargetPeriodInSeconds int `json:"sampling_target_period_in_seconds"` ServerSideConfig struct { TransactionTracerEnabled *bool `json:"transaction_tracer.enabled"` // TransactionTracerThreshold should contain either a number or // "apdex_f" if it is non-nil. TransactionTracerThreshold interface{} `json:"transaction_tracer.transaction_threshold"` TransactionTracerStackTraceThreshold *float64 `json:"transaction_tracer.stack_trace_threshold"` ErrorCollectorEnabled *bool `json:"error_collector.enabled"` ErrorCollectorIgnoreStatusCodes []int `json:"error_collector.ignore_status_codes"` ErrorCollectorExpectStatusCodes []int `json:"error_collector.expected_status_codes"` CrossApplicationTracerEnabled *bool `json:"cross_application_tracer.enabled"` } `json:"agent_config"` // Faster Event Harvest EventData EventHarvestConfig `json:"event_harvest_config"` SpanEventHarvestConfig `json:"span_event_harvest_config"` } // EventHarvestConfig contains fields relating to faster event harvest. // This structure is used in the connect request (to send up defaults) // and in the connect response (to get the server values). // // https://source.datanerd.us/agents/agent-specs/blob/master/Connect-LEGACY.md#event_harvest_config-hash // https://source.datanerd.us/agents/agent-specs/blob/master/Connect-LEGACY.md#event-harvest-config type EventHarvestConfig struct { ReportPeriodMs int `json:"report_period_ms,omitempty"` Limits struct { TxnEvents *uint `json:"analytic_event_data,omitempty"` CustomEvents *uint `json:"custom_event_data,omitempty"` LogEvents *uint `json:"log_event_data,omitempty"` ErrorEvents *uint `json:"error_event_data,omitempty"` SpanEvents *uint `json:"span_event_data,omitempty"` } `json:"harvest_limits"` } // SpanEventHarvestConfig contains the Reporting period time and the given harvest limit. type SpanEventHarvestConfig struct { ReportPeriod *uint `json:"report_period_ms"` HarvestLimit *uint `json:"harvest_limit"` } // ConfigurablePeriod returns the Faster Event Harvest configurable reporting period if it is set, or the default // report period otherwise. func (r *ConnectReply) ConfigurablePeriod() time.Duration { ms := DefaultConfigurableEventHarvestMs if nil != r && r.EventData.ReportPeriodMs > 0 { ms = r.EventData.ReportPeriodMs } return time.Duration(ms) * time.Millisecond } func uintPtr(x uint) *uint { return &x } // DefaultEventHarvestConfig provides faster event harvest defaults. func DefaultEventHarvestConfig(maxTxnEvents, maxLogEvents, maxCustomEvents int) EventHarvestConfig { cfg := EventHarvestConfig{} cfg.ReportPeriodMs = DefaultConfigurableEventHarvestMs cfg.Limits.TxnEvents = uintPtr(uint(maxTxnEvents)) cfg.Limits.CustomEvents = uintPtr(uint(maxCustomEvents)) cfg.Limits.LogEvents = uintPtr(uint(maxLogEvents)) cfg.Limits.ErrorEvents = uintPtr(uint(MaxErrorEvents)) return cfg } // DefaultEventHarvestConfigWithDT is an extended version of DefaultEventHarvestConfig, // with the addition that it takes into account distributed tracer span event harvest limits. func DefaultEventHarvestConfigWithDT(maxTxnEvents, maxLogEvents, maxCustomEvents, spanEventLimit int, dtEnabled bool) EventHarvestConfig { cfg := DefaultEventHarvestConfig(maxTxnEvents, maxLogEvents, maxCustomEvents) if dtEnabled { cfg.Limits.SpanEvents = uintPtr(uint(spanEventLimit)) } return cfg } // TrustedAccountSet is used for CAT. type TrustedAccountSet map[int]struct{} // IsTrusted reveals whether the account can be trusted. func (t *TrustedAccountSet) IsTrusted(account int) bool { _, exists := (*t)[account] return exists } // UnmarshalJSON unmarshals the trusted set from the connect reply JSON. func (t *TrustedAccountSet) UnmarshalJSON(data []byte) error { accounts := make([]int, 0) if err := json.Unmarshal(data, &accounts); err != nil { return err } *t = make(TrustedAccountSet) for _, account := range accounts { (*t)[account] = struct{}{} } return nil } // ConnectReplyDefaults returns a newly allocated ConnectReply with the proper // default settings. A pointer to a global is not used to prevent consumers // from changing the default settings. func ConnectReplyDefaults() *ConnectReply { return &ConnectReply{ ApdexThresholdSeconds: 0.5, CollectAnalyticsEvents: true, CollectCustomEvents: true, CollectTraces: true, CollectErrors: true, CollectErrorEvents: true, CollectSpanEvents: true, MaxPayloadSizeInBytes: MaxPayloadSizeInBytes, SamplingTarget: 10, SamplingTargetPeriodInSeconds: 60, TraceIDGenerator: NewTraceIDGenerator(int64(time.Now().UnixNano())), DistributedTraceTimestampGenerator: time.Now, } } // CalculateApdexThreshold calculates the apdex threshold. func CalculateApdexThreshold(c *ConnectReply, txnName string) time.Duration { if t, ok := c.KeyTxnApdex[txnName]; ok { return FloatSecondsToDuration(t) } return FloatSecondsToDuration(c.ApdexThresholdSeconds) } const ( webMetricPrefix = "WebTransaction/Go" backgroundMetricPrefix = "OtherTransaction/Go" ) // CreateFullTxnName uses collector rules and the appropriate metric prefix to // construct the full transaction metric name from the name given by the // consumer. func CreateFullTxnName(input string, reply *ConnectReply, isWeb bool) string { var afterURLRules string if input != "" { afterURLRules = reply.URLRules.Apply(input) if afterURLRules == "" { return "" } } prefix := backgroundMetricPrefix if isWeb { prefix = webMetricPrefix } var beforeNameRules string if strings.HasPrefix(afterURLRules, "/") { beforeNameRules = prefix + afterURLRules } else { beforeNameRules = prefix + "/" + afterURLRules } afterNameRules := reply.TxnNameRules.Apply(beforeNameRules) if afterNameRules == "" { return "" } return reply.SegmentTerms.apply(afterNameRules) } // RequestEventLimits sets limits for reservior testing type RequestEventLimits struct { CustomEvents int } const ( // CustomEventHarvestsPerMinute is the number of times per minute custom events are harvested CustomEventHarvestsPerMinute = 5 ) // IsConnectedToNewRelic returns true if the connect reply is a valid connect reply // from a New Relic connect endpoint. This is determined by the presence of a RunID // and an EntityGUID which the agent needs to send data to a collector. func (r *ConnectReply) IsConnectedToNewRelic() bool { return r != nil && r.RunID != "" && r.EntityGUID != "" } // MockConnectReplyEventLimits sets up a mock connect reply to test event limits // currently only verifies custom insights events func (r *ConnectReply) MockConnectReplyEventLimits(limits *RequestEventLimits) { r.SetSampleEverything() r.EventData.Limits.CustomEvents = uintPtr(uint(limits.CustomEvents) / (60 / CustomEventHarvestsPerMinute)) // The mock server will be limited to a maximum of 100,000 events per minute if limits.CustomEvents > 100000 { r.EventData.Limits.CustomEvents = uintPtr(uint(100000) / (60 / CustomEventHarvestsPerMinute)) } if limits.CustomEvents <= 0 { r.EventData.Limits.CustomEvents = uintPtr(uint(0) / (60 / CustomEventHarvestsPerMinute)) } } // SetSampleEverything is used for testing to ensure span events get saved. func (r *ConnectReply) SetSampleEverything() { // These constants are not large enough to sample everything forever, // but should satisfy our tests! r.SamplingTarget = 1000 * 1000 * 1000 r.SamplingTargetPeriodInSeconds = 1000 * 1000 * 1000 } // SetSampleNothing is used for testing to ensure no span events get saved. func (r *ConnectReply) SetSampleNothing() { r.SamplingTarget = 0 } // UnmarshalConnectReply takes the body of a Connect reply, in the form of bytes, and a // PreconnectReply, and converts it into a *ConnectReply func UnmarshalConnectReply(body []byte, preconnect PreconnectReply) (*ConnectReply, error) { var reply struct { Reply *ConnectReply `json:"return_value"` } reply.Reply = ConnectReplyDefaults() err := json.Unmarshal(body, &reply) if nil != err { return nil, fmt.Errorf("unable to parse connect reply: %v", err) } reply.Reply.PreconnectReply = preconnect return reply.Reply, nil } go-agent-3.42.0/v3/internal/connect_reply_test.go000066400000000000000000000157771510742411500217200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "fmt" "testing" "time" ) func TestCreateFullTxnNameBasic(t *testing.T) { emptyReply := ConnectReplyDefaults() tcs := []struct { input string background bool expect string }{ {"", true, "WebTransaction/Go/"}, {"/", true, "WebTransaction/Go/"}, {"hello", true, "WebTransaction/Go/hello"}, {"/hello", true, "WebTransaction/Go/hello"}, {"", false, "OtherTransaction/Go/"}, {"/", false, "OtherTransaction/Go/"}, {"hello", false, "OtherTransaction/Go/hello"}, {"/hello", false, "OtherTransaction/Go/hello"}, } for _, tc := range tcs { if out := CreateFullTxnName(tc.input, emptyReply, tc.background); out != tc.expect { t.Error(tc.input, tc.background, out, tc.expect) } } } func TestCreateFullTxnNameURLRulesIgnore(t *testing.T) { js := `[{ "match_expression":".*zip.*$", "ignore":true }]` reply := ConnectReplyDefaults() err := json.Unmarshal([]byte(js), &reply.URLRules) if nil != err { t.Fatal(err) } if out := CreateFullTxnName("/zap/zip/zep", reply, true); out != "" { t.Error(out) } } func TestCreateFullTxnNameTxnRulesIgnore(t *testing.T) { js := `[{ "match_expression":"^WebTransaction/Go/zap/zip/zep$", "ignore":true }]` reply := ConnectReplyDefaults() err := json.Unmarshal([]byte(js), &reply.TxnNameRules) if nil != err { t.Fatal(err) } if out := CreateFullTxnName("/zap/zip/zep", reply, true); out != "" { t.Error(out) } } func TestCalculateApdexThreshold(t *testing.T) { reply := ConnectReplyDefaults() threshold := CalculateApdexThreshold(reply, "WebTransaction/Go/hello") if threshold != 500*time.Millisecond { t.Error("default apdex threshold", threshold) } reply = ConnectReplyDefaults() reply.ApdexThresholdSeconds = 1.3 reply.KeyTxnApdex = map[string]float64{ "WebTransaction/Go/zip": 2.2, "WebTransaction/Go/zap": 2.3, } threshold = CalculateApdexThreshold(reply, "WebTransaction/Go/hello") if threshold != 1300*time.Millisecond { t.Error(threshold) } threshold = CalculateApdexThreshold(reply, "WebTransaction/Go/zip") if threshold != 2200*time.Millisecond { t.Error(threshold) } } func TestIsTrusted(t *testing.T) { for _, test := range []struct { id int trusted string expected bool }{ {1, `[]`, false}, {1, `[2, 3]`, false}, {1, `[1]`, true}, {1, `[1, 2, 3]`, true}, } { trustedAccounts := make(TrustedAccountSet) if err := json.Unmarshal([]byte(test.trusted), &trustedAccounts); err != nil { t.Fatal(err) } if actual := trustedAccounts.IsTrusted(test.id); test.expected != actual { t.Errorf("failed asserting whether %d is trusted by %v: expected %v; got %v", test.id, test.trusted, test.expected, actual) } } } func BenchmarkDefaultRules(b *testing.B) { js := `{"url_rules":[ { "match_expression":".*\\.(ace|arj|ini|txt|udl|plist|css|gif|ico|jpe?g|js|png|swf|woff|caf|aiff|m4v|mpe?g|mp3|mp4|mov)$", "replacement":"/*.\\1", "ignore":false, "eval_order":1000, "terminate_chain":true, "replace_all":false, "each_segment":false }, { "match_expression":"^[0-9][0-9a-f_,.-]*$", "replacement":"*", "ignore":false, "eval_order":1001, "terminate_chain":false, "replace_all":false, "each_segment":true }, { "match_expression":"^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$", "replacement":"\\1/.*\\2", "ignore":false, "eval_order":1002, "terminate_chain":false, "replace_all":false, "each_segment":false } ]}` reply := ConnectReplyDefaults() err := json.Unmarshal([]byte(js), &reply) if nil != err { b.Fatal(err) } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { if out := CreateFullTxnName("/myEndpoint", reply, true); out != "WebTransaction/Go/myEndpoint" { b.Error(out) } } } func TestNegativeHarvestLimits(t *testing.T) { // Test that negative harvest event limits will cause a connect error. // Harvest event limits are never expected to be negative: This is just // extra defensiveness. _, err := UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "harvest_limits": { "error_event_data": -1 } } }}`), PreconnectReply{}) if err == nil { t.Fatal("expected error missing") } } func TestDefaultEventHarvestConfigJSON(t *testing.T) { js, err := json.Marshal(DefaultEventHarvestConfig(MaxTxnEvents, MaxLogEvents, MaxCustomEvents)) if err != nil { t.Error(err) } expect := fmt.Sprintf(`{"report_period_ms":60000,"harvest_limits":{"analytic_event_data":10000,"custom_event_data":%d,"log_event_data":%d,"error_event_data":100}}`, MaxCustomEvents, MaxLogEvents) if string(js) != expect { t.Errorf("DefaultEventHarvestConfig does not match expected valued:\nExpected:\t%s\nActual:\t\t%s", expect, string(js)) } } func TestConnectReply_IsConnectedToNewRelic(t *testing.T) { reply := ConnectReplyDefaults() if reply.IsConnectedToNewRelic() { t.Error("Connect Reply Defaults should not be considered connected to New Relic") } reply = ConnectReplyDefaults() reply.RunID = "foo" reply.EntityGUID = "bar" if !reply.IsConnectedToNewRelic() { t.Error("Connect Reply with RunID and EntityGUID should be considered connected to New Relic") } } func TestAgentRunIdString(t *testing.T) { id := AgentRunID("agent-run-id") if id.String() != "agent-run-id" { t.Errorf("AgentRunID did not match expected value: %v", id) } } func TestDefaultEventHarvestConfigWithDT(t *testing.T) { cfg := DefaultEventHarvestConfigWithDT(1, 2, 3, 4, true) ce := *cfg.Limits.CustomEvents == 3 ee := *cfg.Limits.ErrorEvents == 100 le := *cfg.Limits.LogEvents == 2 se := *cfg.Limits.SpanEvents == 4 txe := *cfg.Limits.TxnEvents == 1 rpms := cfg.ReportPeriodMs == 60*1000 if !ce && !ee && !le && !se && !txe && !rpms { t.Errorf("DefaultEventHarvestConfigWithDT does not match expected value: %v", cfg) } } func TestConnectReplyMockConnectReplyEventLimitsWithGreaterThanMaxLimit(t *testing.T) { ehc := DefaultEventHarvestConfigWithDT(1, 2, 3, 4, true) cr := &ConnectReply{EventData: ehc} rel := &RequestEventLimits{CustomEvents: 100001} cr.MockConnectReplyEventLimits(rel) expected := uint(8333) if *cr.EventData.Limits.CustomEvents != expected { t.Errorf("ConnectReply.EventData.Limits.CustomEvents does not match expected value: %v", expected) } } func TestConnectReplyMockConnectReplyEventLimitsWithLessThanMinLimit(t *testing.T) { ehc := DefaultEventHarvestConfigWithDT(1, 2, 3, 4, true) cr := &ConnectReply{EventData: ehc} rel := &RequestEventLimits{CustomEvents: -1} cr.MockConnectReplyEventLimits(rel) expected := uint(0) if *cr.EventData.Limits.CustomEvents != expected { t.Errorf("ConnectReply.EventData.Limits.CustomEvents does not match expected value: %v", expected) } } func TestConnectReplyMockConnectReplySampleNothing(t *testing.T) { cr := &ConnectReply{SamplingTarget: 100} cr.SetSampleNothing() expected := uint64(0) if cr.SamplingTarget != expected { t.Errorf("ConnectReply.SamplingTarget does not match expected value: %v", expected) } } go-agent-3.42.0/v3/internal/context.go000066400000000000000000000013321510742411500174570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal type contextKeyType struct{} var ( // TransactionContextKey is the key used for newrelic.FromContext and // newrelic.NewContext. TransactionContextKey = contextKeyType(struct{}{}) // GinTransactionContextKey is used as the context key in // nrgin.Middleware and nrgin.Transaction. Unfortunately, Gin requires // a string context key. We use two different context keys (and check // both in nrgin.Transaction and newrelic.FromContext) rather than use a // single string key because context.WithValue will fail golint if used // with a string key. GinTransactionContextKey = "newRelicTransaction" ) go-agent-3.42.0/v3/internal/crossagent/000077500000000000000000000000001510742411500176155ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/README.md000066400000000000000000000001101510742411500210640ustar00rootroot00000000000000# Cross Agent Tests At commit a234030dc659a6e6d2b920bcc0dc7e25beb520ef go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/000077500000000000000000000000001510742411500233465ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/README.md000066400000000000000000000107751510742411500246370ustar00rootroot00000000000000# Cross Agent Tests ### Data Policy None of these tests should contain customer data such as SQL strings. Please be careful when adding new tests from real world failures. ### Access Push access to this repository is granted via membership in the agents GHE group. ### Tests | Test Files | Description | | ------------- |-------------| | [rum_loader_insertion_location](rum_loader_insertion_location) | Describe where the RUM loader (formerly known as header) should be inserted. | | [rum_footer_insertion_location](rum_footer_insertion_location) | Describe where the RUM footer (aka "client config") should be inserted. These tests do not apply to agents which insert the footer directly after the loader. | | [rules.json](rules.json) | Describe how url/metric/txn-name rules should be applied. | | [rum_client_config.json](rum_client_config.json) | These tests dictate the format and contents of the browser monitoring client configuration. For more information see: [SPEC](https://newrelic.atlassian.net/wiki/display/eng/BAM+Agent+Auto-Instrumentation) | | [sql_parsing.json](sql_parsing.json) | These tests show how an SQL string should be parsed for the operation and table name. *Java Note*: The Java Agent is [out-of-sync with these tests](https://source.datanerd.us/java-agent/java_agent/blob/master/newrelic-agent/src/main/java/com/newrelic/agent/database/DefaultDatabaseStatementParser.java), [has its own tests](https://source.datanerd.us/java-agent/java_agent/blob/master/newrelic-agent/src/test/java/com/newrelic/agent/database/DatabaseStatementResponseParserTest.java), and cannot implement these without a breaking change. | | [url_clean.json](url_clean.json) | These tests show how URLs should be cleaned before putting them into a trace segment's parameter hash (under the key 'uri'). | | [url_domain_extraction.json](url_domain_extraction.json) | These tests show how the domain of a URL should be extracted (for the purpose of creating external metrics). | | [postgres_explain_obfuscation](postgres_explain_obfuscation) | These tests show how plain-text explain plan output from PostgreSQL should be obfuscated when SQL obfuscation is enabled. | | [sql_obfuscation](sql_obfuscation) | Describe how agents should obfuscate SQL queries before transmission to the collector. | | [attribute_configuration](attribute_configuration.json) | These tests show how agents should respond to the various attribute configuration settings. For more information see: [Attributes SPEC](https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md) | | [cat](cat) | These tests cover the new Dirac attributes that are added for the CAT Map project. See the [CAT Spec](https://source.datanerd.us/agents/agent-specs/blob/master/Cross-Application-Tracing-PORTED.md) and the [README](cat/README.md) for details.| | [labels](labels.json) | These tests cover the Labels for Language Agents project. See the [Labels for Language Agents Spec](https://newrelic.atlassian.net/wiki/display/eng/Labels+for+Language+Agents) for details.| | [proc_cpuinfo](proc_cpuinfo) | These test correct processing of `/proc/cpuinfo` output on Linux hosts. | | [proc_meminfo](proc_meminfo) | These test correct processing of `/proc/meminfo` output on Linux hosts. | | [transaction_segment_terms.json](transaction_segment_terms.json) | These tests cover agent implementations of the `transaction_segment_terms` transaction renaming rules introduced in collector protocol 14. See [the spec](https://newrelic.atlassian.net/wiki/display/eng/Language+agent+transaction+segment+terms+rules) for details. | | [synthetics](synthetics) | These tests cover agent support for Synthetics. For details, see [Agent Support for Synthetics: Forced Transaction Traces and Analytic Events](https://source.datanerd.us/agents/agent-specs/blob/master/Synthetics-PORTED.md). | | [docker_container_id](docker_container_id) | These tests cover parsing of Docker container IDs from `/proc/*/cgroup` on Linux hosts. | | [utilization](utilization) | These tests cover the collection and validation of metadata for billing purposes as per the [Utilization spec](https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md). | | [utilization_vendor_specific](utilization_vendor_specific) | These tests cover the collection and validation of metadata for AWS, Pivotal Cloud Foundry, Google Cloud Platform, and Azure as per the [Utilization spec](https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md). | | [distributed_tracing](distributed_tracing) | distributed tracing, a.k.a. CAT CATs | go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/attribute_configuration.json000066400000000000000000000405641510742411500312040ustar00rootroot00000000000000[{ "testname": "everything enabled, no include/exclude", "config": { "browser_monitoring.attributes.enabled": true }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "browser monitoring attributes disabled by default", "config": {}, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector" ] }, { "testname": "attributes globally disabled", "config": { "attributes.enabled": false, "transaction_events.attributes.enabled": true, "transaction_tracer.attributes.enabled": true, "error_collector.attributes.enabled": true, "browser_monitoring.attributes.enabled": true }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "all categories disabled", "config": { "transaction_events.attributes.enabled": false, "transaction_tracer.attributes.enabled": false, "error_collector.attributes.enabled": false }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "global exclude", "config": { "attributes.exclude": ["alpha"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "exclude in each category", "config": { "transaction_events.attributes.exclude": ["alpha"], "transaction_tracer.attributes.exclude": ["alpha"], "error_collector.attributes.exclude": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.exclude": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "global include", "config": { "attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "each category include", "config": { "transaction_events.attributes.include": ["alpha"], "transaction_tracer.attributes.include": ["alpha"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.include": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "global include/exclude contradict", "config": { "attributes.exclude": ["alpha"], "attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "include/exclude contradict in each category", "config": { "transaction_events.attributes.exclude": ["alpha"], "transaction_events.attributes.include": ["alpha"], "transaction_tracer.attributes.exclude": ["alpha"], "transaction_tracer.attributes.include": ["alpha"], "error_collector.attributes.exclude": ["alpha"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.exclude": ["alpha"], "browser_monitoring.attributes.include": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "global exclude contradicts category include", "config": { "attributes.exclude": ["alpha"], "transaction_events.attributes.include": ["alpha"], "transaction_tracer.attributes.include": ["alpha"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.include": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "global include contradicts category exclude", "config": { "attributes.include": ["alpha"], "transaction_events.attributes.exclude": ["alpha"], "transaction_tracer.attributes.exclude": ["alpha"], "error_collector.attributes.exclude": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.exclude": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "alpha is more specific than alpha*", "config": { "attributes.include": ["alpha"], "transaction_events.attributes.exclude": ["alpha*"], "transaction_tracer.attributes.exclude": ["alpha*"], "error_collector.attributes.exclude": ["alpha*"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.exclude": ["alpha*"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "all destination modifiers applied, not only the most specific one", "config": { "attributes.exclude": ["a*"], "transaction_events.attributes.include": ["ab*"], "transaction_events.attributes.exclude": ["abc*"], "transaction_tracer.attributes.exclude": ["abcd*"], "transaction_tracer.attributes.include": ["abcde*"], "error_collector.attributes.include": ["abcdef*"], "error_collector.attributes.exclude": ["abcdefg*"], "browser_monitoring.attributes.exclude": ["abcdefgh*"], "browser_monitoring.attributes.include": ["abcdefghi*"], "browser_monitoring.attributes.enabled": true }, "input_key": "abcdefghik", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_tracer", "browser_monitoring" ] }, { "testname": "venn diagram part 1", "config": { "attributes.exclude": ["alpha.*", "alpha.beta.gamma.*"], "attributes.include": ["alpha.beta.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "venn diagram part 2", "config": { "attributes.exclude": ["alpha.*", "alpha.beta.gamma.*"], "attributes.include": ["alpha.beta.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.psi", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "venn diagram part 3", "config": { "attributes.exclude": ["alpha.*", "alpha.beta.gamma.*"], "attributes.include": ["alpha.beta.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.beta.", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "venn diagram part 4", "config": { "attributes.exclude": ["alpha.*", "alpha.beta.gamma.*"], "attributes.include": ["alpha.beta.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.beta.psi", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "venn diagram part 5", "config": { "attributes.exclude": ["alpha.*", "alpha.beta.gamma.*"], "attributes.include": ["alpha.beta.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.beta.gamma.", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "alpha is not mistaken for alpha*", "config": { "transaction_events.attributes.include": ["alpha"], "transaction_tracer.attributes.exclude": ["alpha*"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.exclude": ["alpha*"], "browser_monitoring.attributes.enabled": true }, "input_key": "alpha.beta", "input_default_destinations": [ "transaction_tracer", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "exact match is case sensitive", "config": { "attributes.exclude": ["alpha"], "browser_monitoring.attributes.enabled": true }, "input_key": "ALPHA", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "wildcard match is case sensitive", "config": { "attributes.exclude": ["alpha.*"], "browser_monitoring.attributes.enabled": true }, "input_key": "ALPHA.BETA", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "include with attributes globally disabled", "config": { "attributes.enabled": false, "transaction_events.attributes.include": ["alpha"], "transaction_tracer.attributes.include": ["alpha"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.include": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ ] }, { "testname": "include with disabled destinations", "config": { "transaction_events.attributes.include": ["alpha"], "transaction_events.attributes.enabled": false, "transaction_tracer.attributes.include": ["alpha"], "error_collector.attributes.include": ["alpha"], "browser_monitoring.attributes.enabled": true, "browser_monitoring.attributes.include": ["alpha"] }, "input_key": "alpha", "input_default_destinations": [ "transaction_events", "transaction_tracer", "error_collector", "browser_monitoring" ], "expected_destinations": [ "transaction_tracer", "error_collector", "browser_monitoring" ] }, { "testname": "ordering of rules should not matter 1", "config": { "transaction_events.attributes.include": ["b*", "bcd*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "b", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 2", "config": { "transaction_events.attributes.include": ["b*", "bcd*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bc", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 3", "config": { "transaction_events.attributes.include": ["b*", "bcd*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bcd", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 4", "config": { "transaction_events.attributes.include": ["b*", "bcd*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bcde", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 5", "config": { "transaction_events.attributes.include": ["bcd*", "b*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "b", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 6", "config": { "transaction_events.attributes.include": ["bcd*", "b*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bc", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 7", "config": { "transaction_events.attributes.include": ["bcd*", "b*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bcd", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] }, { "testname": "ordering of rules should not matter 8", "config": { "transaction_events.attributes.include": ["bcd*", "b*"], "transaction_events.attributes.exclude": ["bc*"] }, "input_key": "bcde", "input_default_destinations": [ "transaction_events", "transaction_tracer" ], "expected_destinations": [ "transaction_events", "transaction_tracer" ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/cat/000077500000000000000000000000001510742411500241155ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/cat/README.md000066400000000000000000000045171510742411500254030ustar00rootroot00000000000000### CAT Map test details The CAT map test cases in `cat_map.json` are meant to be used to verify the attributes that agents collect and attach to analytics transaction events for the CAT map project. **NOTE** currently `nr.apdexPerfZone` is not covered by these tests, make sure you test for this yourself until it is added to these tests. Each test case should correspond to a simulated transaction in the agent under test. Here's what the various fields in each test case mean: | Name | Meaning | | ---- | ------- | | `name` | A human-meaningful name for the test case. | | `appName` | The name of the New Relic application for the simulated transaction. | | `transactionName` | The final name of the simulated transaction. | | `transactionGuid` | The GUID of the simulated transaction. | | `inboundPayload` | The (non-serialized) contents of the `X-NewRelic-Transaction` HTTP request header on the simulated transaction. Note that this value should be serialized to JSON, obfuscated using the CAT obfuscation algorithm, and Base64-encoded before being used in the header value. Note also that the `X-NewRelic-ID` header should be set on the simulated transaction, though its value is not specified in these tests. | | `expectedIntrinsicFields` | A set of key-value pairs that are expected to be present in the analytics event generated for the simulated transaction. These fields should be present in the first hash of the analytic event payload (built-in agent-supplied fields). | | `nonExpectedIntrinsicFields` | An array of attribute names that should *not* be present in the analytics event generated for the simulated transaction. | | `outboundRequests` | An array of objects representing outbound requests that should be made in the context of the simulated transaction. See the table below for details. Only present if the test case involves making outgoing requests from the simulated transaction. | Here's what the fields of each entry in the `outboundRequests` array mean: | Name | Meaning | | ---- | ------- | | `outboundTxnName` | The name of the simulated transaction at the time this outbound request is made. Your test driver should set the transaction name to this value prior to simulating the outbound request. | | `expectedOutboundPayload` | The expected (un-obfuscated) content of the outbound `X-NewRelic-Transaction` request header for this request. | go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/cat/cat_map.json000066400000000000000000000365021510742411500264220ustar00rootroot00000000000000[ { "name": "new_cat", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", "5d2957be" ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "815b96d3", "nr.referringTransactionGuid": "b854df4feb2b1f06", "nr.referringPathHash": "5d2957be" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes" ] }, { "name": "new_cat_path_hash_with_leading_zero", "appName": "testAppName", "transactionName": "WebTransaction/Custom/txn4", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", "5d2957be" ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "0e258e4e", "nr.referringTransactionGuid": "b854df4feb2b1f06", "nr.referringPathHash": "5d2957be" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes" ] }, { "name": "new_cat_path_hash_with_unicode_name", "appName": "testAppName", "transactionName": "WebTransaction/Custom/txn\u221a\u221a\u221a", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", "5d2957be" ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "3d015d23", "nr.referringTransactionGuid": "b854df4feb2b1f06", "nr.referringPathHash": "5d2957be" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes" ] }, { "name": "new_cat_no_referring_payload", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": null, "expectedIntrinsicFields": {}, "nonExpectedIntrinsicFields": [ "nr.guid", "nr.tripId", "nr.pathHash", "nr.referringTransactionGuid", "nr.referringPathHash", "nr.alternatePathHashes" ] }, { "name": "new_cat_with_call_out", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": null, "outboundRequests": [ { "outboundTxnName": "WebTransaction/Custom/testTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "3b0939af" ] } ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af" }, "nonExpectedIntrinsicFields": [ "nr.referringTransactionGuid", "nr.referringPathHash", "nr.alternatePathHashes" ] }, { "name": "new_cat_with_multiple_calls_out", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": null, "outboundRequests": [ { "outboundTxnName": "WebTransaction/Custom/otherTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "f1c8adf5" ] }, { "outboundTxnName": "WebTransaction/Custom/otherTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "f1c8adf5" ] }, { "outboundTxnName": "WebTransaction/Custom/moreOtherTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "ea19b61c" ] }, { "outboundTxnName": "WebTransaction/Custom/moreDifferentTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "e00736cc" ] } ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af", "nr.alternatePathHashes": "e00736cc,ea19b61c,f1c8adf5" }, "nonExpectedIntrinsicFields": [ "nr.referringTransactionGuid", "nr.referringPathHash" ] }, { "name": "new_cat_with_many_unique_calls_out", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": null, "outboundRequests": [ { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn2", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "a67c2da4" ] }, { "outboundTxnName": "WebTransaction/Custom/txn3", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "0d932b2b" ] }, { "outboundTxnName": "WebTransaction/Custom/txn4", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "b4772132" ] }, { "outboundTxnName": "WebTransaction/Custom/txn5", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "51a1a337" ] }, { "outboundTxnName": "WebTransaction/Custom/txn6", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "77b5cb70" ] }, { "outboundTxnName": "WebTransaction/Custom/txn7", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "8a842c7f" ] }, { "outboundTxnName": "WebTransaction/Custom/txn8", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "b968edb8" ] }, { "outboundTxnName": "WebTransaction/Custom/txn9", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "2691f90e" ] }, { "outboundTxnName": "WebTransaction/Custom/txn10", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "b46aec87" ] }, { "outboundTxnName": "WebTransaction/Custom/txn11", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "10bb3bf3" ] } ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af", "nr.alternatePathHashes": "0d932b2b,2691f90e,51a1a337,77b5cb70,8a842c7f,93fb4310,a67c2da4,b46aec87,b4772132,b968edb8" }, "nonExpectedIntrinsicFields": [ "nr.referringTransactionGuid", "nr.referringPathHash" ] }, { "name": "new_cat_with_many_calls_out", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": null, "outboundRequests": [ { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn1", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "93fb4310" ] }, { "outboundTxnName": "WebTransaction/Custom/txn2", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "9323dc260548ed0e", "a67c2da4" ] } ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af", "nr.alternatePathHashes": "93fb4310,a67c2da4" }, "nonExpectedIntrinsicFields": [ "nr.referringTransactionGuid", "nr.referringPathHash" ] }, { "name": "new_cat_with_referring_info_and_call_out", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", "5d2957be" ], "outboundRequests": [ { "outboundTxnName": "WebTransaction/Custom/otherTxnName", "expectedOutboundPayload": [ "9323dc260548ed0e", false, "7e249074f277923d", "4b9a0289" ] } ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "815b96d3", "nr.alternatePathHashes": "4b9a0289", "nr.referringTransactionGuid": "b854df4feb2b1f06", "nr.referringPathHash": "5d2957be" }, "nonExpectedIntrinsicFields": [] }, { "name": "new_cat_missing_path_hash", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d" ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "3b0939af", "nr.referringTransactionGuid": "b854df4feb2b1f06" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes", "nr.referringPathHash" ] }, { "name": "new_cat_null_path_hash", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", null ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "3b0939af", "nr.referringTransactionGuid": "b854df4feb2b1f06" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes", "nr.referringPathHash" ] }, { "name": "new_cat_malformed_path_hash", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", [ "scrambled", "eggs" ] ], "expectedIntrinsicFields": {}, "nonExpectedIntrinsicFields": [ "nr.guid", "nr.tripId", "nr.pathHash", "nr.referringTransactionGuid", "nr.referringPathHash", "nr.alternatePathHashes" ] }, { "name": "new_cat_corrupt_path_hash", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, "7e249074f277923d", "ZXYQEDABC" ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "7e249074f277923d", "nr.pathHash": "3b0939af", "nr.referringTransactionGuid": "b854df4feb2b1f06", "nr.referringPathHash": "ZXYQEDABC" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes" ] }, { "name": "new_cat_malformed_trip_id", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, ["scrambled"], "5d2957be" ], "expectedIntrinsicFields": {}, "nonExpectedIntrinsicFields": [ "nr.guid", "nr.tripId", "nr.pathHash", "nr.referringTransactionGuid", "nr.referringPathHash", "nr.alternatePathHashes" ] }, { "name": "new_cat_missing_trip_id", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af", "nr.referringTransactionGuid": "b854df4feb2b1f06" }, "nonExpectedIntrinsicFields": [ "nr.referringPathHash", "nr.alternatePathHashes" ] }, { "name": "new_cat_null_trip_id", "appName": "testAppName", "transactionName": "WebTransaction/Custom/testTxnName", "transactionGuid": "9323dc260548ed0e", "inboundPayload": [ "b854df4feb2b1f06", false, null ], "expectedIntrinsicFields": { "nr.guid": "9323dc260548ed0e", "nr.tripId": "9323dc260548ed0e", "nr.pathHash": "3b0939af", "nr.referringTransactionGuid": "b854df4feb2b1f06" }, "nonExpectedIntrinsicFields": [ "nr.alternatePathHashes", "nr.referringPathHash" ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/cat/path_hashing.json000066400000000000000000000026021510742411500274450ustar00rootroot00000000000000[ { "name": "no referring path hash", "referringPathHash": null, "applicationName": "application A", "transactionName": "transaction A", "expectedPathHash": "5e17050e" }, { "name": "leading zero on resulting path hash", "referringPathHash": null, "applicationName": "my application", "transactionName": "transaction 13", "expectedPathHash": "097ca5e1" }, { "name": "with referring path hash", "referringPathHash": "95f2f716", "applicationName": "app2", "transactionName": "txn2", "expectedPathHash": "ef72c2e6" }, { "name": "with referring path hash leading zero", "referringPathHash": "077634eb", "applicationName": "app3", "transactionName": "txn3", "expectedPathHash": "bfd6587f" }, { "name": "with multi-byte UTF-8 characters in transaction name", "referringPathHash": "95f2f716", "applicationName": "app1", "transactionName": "Доверяй, но проверяй", "expectedPathHash": "b7ad900e" }, { "name": "high bit of referringPathHash set", "referringPathHash": "80000000", "applicationName": "app1", "transactionName": "txn1", "expectedPathHash": "95f2f717" }, { "name": "low bit of referringPathHash set", "referringPathHash": "00000001", "applicationName": "app1", "transactionName": "txn1", "expectedPathHash": "95f2f714" } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/collector_hostname.json000066400000000000000000000046611510742411500301340ustar00rootroot00000000000000[ { "name": "normal license key", "config_file_key": "08a2ad66c637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.newrelic.com" }, { "name": "region aware key with four character identifier", "config_file_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.eu01.nr-data.net" }, { "name": "region aware key with five character identifier", "config_file_key": "gov01x66c637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.gov01.nr-data.net" }, { "name": "region aware key with seven character identifier", "config_file_key": "foo1234xc637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.foo1234.nr-data.net" }, { "name": "region aware key with abnormal identifier", "config_file_key": "20foox66c637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.20foo.nr-data.net" }, { "name": "region aware key with more than one identifier", "config_file_key": "eu01xeu02x37a29c3982469a3fe8d1982d002c4a", "hostname": "collector.eu01.nr-data.net" }, { "name": "environment variable specified license key", "env_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "hostname": "collector.eu01.nr-data.net" }, { "name": "env var host override", "config_file_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "env_override_host": "other-collector.newrelic.com", "hostname": "other-collector.newrelic.com" }, { "name": "local config host override", "config_file_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "config_override_host": "other-collector.newrelic.com", "hostname": "other-collector.newrelic.com" }, { "name": "local config host override with env key", "env_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "config_override_host": "other-collector.newrelic.com", "hostname": "other-collector.newrelic.com" }, { "name": "env var host override with env key", "env_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "env_override_host": "other-collector.newrelic.com", "hostname": "other-collector.newrelic.com" }, { "name": "env var host override default with default env key", "env_key": "eu01xx66c637a29c3982469a3fe8d1982d002c4a", "env_override_host": "collector.newrelic.com", "hostname": "collector.newrelic.com" }, { "name": "No specified key defaults to collector.newrelic.com", "hostname": "collector.newrelic.com" } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/data_collection_server_configuration.json000066400000000000000000000077641510742411500337200ustar00rootroot00000000000000[ { "test_name": "collect_span_events_disabled", "connect_response": { "collect_span_events": false }, "expected_data_seen": [ { "type": "span_event", "count": 0 } ], "expected_endpoint_calls": [ { "method": "span_event_data", "count": 0 } ] }, { "test_name": "collect_span_events_enabled", "connect_response": { "collect_span_events": true }, "expected_data_seen": [ { "type": "span_event", "count": 1 } ], "expected_endpoint_calls": [ { "method": "span_event_data", "count": 1 } ] }, { "test_name": "collect_custom_events_disabled", "connect_response": { "collect_custom_events": false }, "expected_data_seen": [ { "type": "custom_event", "count": 0 } ], "expected_endpoint_calls": [ { "method": "custom_event_data", "count": 0 } ] }, { "test_name": "collect_custom_events_enabled", "connect_response": { "collect_custom_events": true }, "expected_data_seen": [ { "type": "custom_event", "count": 1 } ], "expected_endpoint_calls": [ { "method": "custom_event_data", "count": 1 } ] }, { "test_name": "collect_analytics_events_disabled", "connect_response": { "collect_analytics_events": false }, "expected_data_seen": [ { "type": "transaction_event", "count": 0 } ], "expected_endpoint_calls": [ { "method": "analytic_event_data", "count": 0 } ] }, { "test_name": "collect_analytics_events_enabled", "connect_response": { "collect_analytics_events": true }, "expected_data_seen": [ { "type": "transaction_event", "count": 1 } ], "expected_endpoint_calls": [ { "method": "analytic_event_data", "count": 1 } ] }, { "test_name": "collect_error_events_disabled", "connect_response": { "collect_error_events": false }, "expected_data_seen": [ { "type": "error_event", "count": 0 } ], "expected_endpoint_calls": [ { "method": "error_event_data", "count": 0 } ] }, { "test_name": "collect_error_events_enabled", "connect_response": { "collect_error_events": true }, "expected_data_seen": [ { "type": "error_event", "count": 1 } ], "expected_endpoint_calls": [ { "method": "error_event_data", "count": 1 } ] }, { "test_name": "collect_errors_disabled", "connect_response": { "collect_errors": false }, "expected_data_seen": [ { "type": "error_trace", "count": 0 } ], "expected_endpoint_calls": [ { "method": "error_data", "count": 0 } ] }, { "test_name": "collect_errors_enabled", "connect_response": { "collect_errors": true }, "expected_data_seen": [ { "type": "error_trace", "count": 1 } ], "expected_endpoint_calls": [ { "method": "error_data", "count": 1 } ] }, { "test_name": "collect_traces_disabled", "connect_response": { "collect_traces": false }, "expected_data_seen": [ { "type": "transaction_trace", "count": 0 } ], "expected_endpoint_calls": [ { "method": "transaction_sample_data", "count": 0 } ] }, { "test_name": "collect_traces_enabled", "connect_response": { "collect_traces": true }, "expected_data_seen": [ { "type": "transaction_trace", "count": 1 } ], "expected_endpoint_calls": [ { "method": "transaction_sample_data", "count": 1 } ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/datastores/000077500000000000000000000000001510742411500255175ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/datastores/README.md000066400000000000000000000034071510742411500270020ustar00rootroot00000000000000## Datastore instance tests The datastore instance tests provide attributes similar to what an agent could expect to find regarding a database configuration and specifies the expected [datastore instance metric](https://source.datanerd.us/agents/agent-specs/blob/master/Datastore-Metrics-PORTED.md#datastore-metric-namespace) that should be generated. The table below lists types attributes and whether will will always be included or optionally included in each test case. | Name | Present | Description | |---|---|---| | system_hostname | always | the hostname of the machine | | db_hostname | sometimes | the hostname reported by the database adapter | | product | always | the database product for this configuration | port | sometimes | the port reported by the database adapter | | unix_socket | sometimes |the path to a unix domain socket reported by a database adapter | | database_path | sometimes |the path to a filesystem database | | expected\_instance\_metric | always | the instance metric expected to be generated from the given attributes | ## Implementing the test cases The idea behind these test cases are that you are able to determine a set of configuration properties from a database connection, and based on those properties you should generate the `expected_instance_metric`. Sometimes the properties available are minimal and will mean that you will need to fall back to defaults to obtain some of the information. When there is missing information from a database adapter the guiding principle is to fill in the defaults when they can be inferred, but do not make guesses that could be incorrect or misleading. Some agents may have access to better data and may not need to make inferences. If this applies to your agent then many of these tests will not be applicable. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/datastores/datastore_api.json000066400000000000000000000305631510742411500312400ustar00rootroot00000000000000[ { "test_name": "all required fields present, everything enabled", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" } } }, { "test_name": "database name missing", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"3306" } } }, { "test_name": "host and port missing", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "database_name":"my_db" } } }, { "test_name": "host missing, but port present", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/unknown/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"unknown", "port_path_or_id":"3306", "database_name":"my_db" } } }, { "test_name": "instance reporting false", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":false, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "database_name":"my_db" } } }, { "test_name": "database name disabled", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":false } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"3306" } } }, { "test_name": "all fields missing", "input":{ "parameters":{ }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/Unknown/all", "Datastore/Unknown/allWeb", "Datastore/operation/Unknown/other" ], "metrics_scoped":[ "Datastore/operation/Unknown/other" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/operation/Unknown/other" } } }, { "test_name": "missing collection", "input":{ "parameters":{ "product":"MySQL", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/instance/MySQL/db-server-1/3306" ], "metrics_scoped":[ "Datastore/operation/MySQL/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/operation/MySQL/INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" } } }, { "test_name": "host present, port missing", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1/unknown" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"unknown", "database_name":"my_db" } } }, { "test_name": "localhost replacement", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"localhost", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/datanerd-01/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"datanerd-01", "port_path_or_id":"3306", "database_name":"my_db" } } }, { "test_name": "background transaction", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" }, "is_web":false, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allOther", "Datastore/MySQL/all", "Datastore/MySQL/allOther", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1/3306" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"3306", "database_name":"my_db" } } }, { "test_name": "socket path port", "input":{ "parameters":{ "product":"MySQL", "collection":"users", "operation":"INSERT", "host":"db-server-1", "port_path_or_id":"/var/mysql/mysql.sock", "database_name":"my_db" }, "is_web":true, "system_hostname":"datanerd-01", "configuration":{ "datastore_tracer.instance_reporting.enabled":true, "datastore_tracer.database_name_reporting.enabled":true } }, "expectation":{ "metrics_unscoped":[ "Datastore/all", "Datastore/allWeb", "Datastore/MySQL/all", "Datastore/MySQL/allWeb", "Datastore/operation/MySQL/INSERT", "Datastore/statement/MySQL/users/INSERT", "Datastore/instance/MySQL/db-server-1//var/mysql/mysql.sock" ], "metrics_scoped":[ "Datastore/statement/MySQL/users/INSERT" ], "transaction_segment_and_slow_query_trace":{ "metric_name":"Datastore/statement/MySQL/users/INSERT", "host":"db-server-1", "port_path_or_id":"/var/mysql/mysql.sock", "database_name":"my_db" } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/datastores/datastore_instances.json000066400000000000000000000050611510742411500324510ustar00rootroot00000000000000[ { "name": "instance metric uses system hostname when db reports localhost", "system_hostname": "datanerd-01", "db_hostname": "localhost", "product": "Postgres", "port": 5432, "expected_instance_metric": "Datastore/instance/Postgres/datanerd-01/5432" }, { "name": "instance metric uses system hostname when db reports host as loopback adapter IPv4", "system_hostname": "datanerd-01", "db_hostname": "127.0.0.1", "product": "Postgres", "port": 5252, "expected_instance_metric": "Datastore/instance/Postgres/datanerd-01/5252" }, { "name": "instance metric uses system hostname when db reports host as loopback adapter IPv6", "system_hostname": "datanerd-01", "db_hostname": "0:0:0:0:0:0:0:1", "product": "Postgres", "port": 5252, "expected_instance_metric": "Datastore/instance/Postgres/datanerd-01/5252" }, { "name": "instance metric uses system hostname when db reports host as loopback adapter IPv6 shorthand", "system_hostname": "datanerd-01", "db_hostname": "::1", "product": "Postgres", "port": 5252, "expected_instance_metric": "Datastore/instance/Postgres/datanerd-01/5252" }, { "name": "instance metric uses system hostname when db reports default host IPv6 shorthand", "system_hostname": "datanerd-01", "db_hostname": "::", "product": "MySQL", "port": 5757, "expected_instance_metric": "Datastore/instance/MySQL/datanerd-01/5757" }, { "name": "instance metric uses db host when not local", "system_hostname": "datanerd-01", "db_hostname": "accounts-db", "product": "MySQL", "port": 8420, "expected_instance_metric": "Datastore/instance/MySQL/accounts-db/8420" }, { "name": "instance metric uses unix socket path if provided", "system_hostname": "datanerd-01", "db_hostname": "localhost", "product": "MySQL", "unix_socket": "/var/mysql/mysql.sock", "expected_instance_metric": "Datastore/instance/MySQL/datanerd-01//var/mysql/mysql.sock" }, { "name": "instance metric with ip v6 host", "system_hostname": "datanerd-01", "db_hostname": "2001:0DB8:AC10:FE01:0000:0000:0000:0000", "product": "Postgres", "port": 5432, "expected_instance_metric": "Datastore/instance/Postgres/2001:0DB8:AC10:FE01:0000:0000:0000:0000/5432" }, { "name": "instance metric for filesystem database", "system_hostname": "datanerd-01", "product": "SQLite", "database_path": "/db/all.sqlite3", "expected_instance_metric": "Datastore/instance/SQLite/datanerd-01//db/all.sqlite3" } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/distributed_tracing/000077500000000000000000000000001510742411500273775ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/distributed_tracing/README.md000066400000000000000000000105341510742411500306610ustar00rootroot00000000000000### Trace Context test details The Trace Context test cases in `trace_context.json` are meant to be used to verify the creation and forwarding of W3C Trace Context headers within the agent and the attributes and metrics that get created during that process. Each test case should correspond to a simulated inbound header or creation of a header in the agent under test. Here's what the various fields in each test case mean: | Name | Meaning | | ---- | ------- | | `test_name` | A human-meaningful name for the test case. | | `trusted_account_key` | The account ids the agent can trust. | | `account_id` | The account id the agent would receive on connect. | | `web_transaction` | Whether the transaction that's tested is a web transaction or not. | | `raises_exception` | Whether to simulate an exception happening within the transaction or not, resulting in a transaction error event. | | `force_sampled_true` | Whether to force a transaction to be sampled or not. | | `transport_type` | The transport type for the inbound request. | | `inbound_headers` | The headers you should mock coming into the agent. | | `outbound_payloads` | The exact/expected/unexpected values for outbound `w3c` headers. | | `intrinsics` | The exact/expected/unexpected attributes for events. | | `expected_metrics` | The expected metrics and associated counts as a result of the test. | | `span_events_enabled` | Whether span events are enabled in the agent or not. | | `transaction_events_enabled` | Whether transaction events are enabled in the agent or not. | The `outbound_payloads` and `intrinsics` field can have nested values, for example: ```javascript ... "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "da8bc8cc6d062849b0efcf3c169afb5a" }, "expected": ["guid"], "unexpected": ["grandparentId"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa", "tracingVendors": "" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, ... ``` `target_events` is paired with the `common` block. So anything in the common block should be checked for any event type in the `target_events` list. So for instance, this test should check that both the Transaction and Span events have a `guid`, both have `da8bc8cc6d062849b0efcf3c169afb5a` as the `traceId`, and both don't have a `grandparentId` attribute. The `Transaction` block means anything in there should only apply to the transaction object. Same for the `Span` block. The same idea goes for the `outbound_payloads` block but will apply specifically for the outbound `traceparent` header and `tracestate` header. `outbound_payloads` may also target `newrelic` headers and follow same basic structure inline with trace context headers, for example: ```javascript ... "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "00000000000000006e2fea0b173fdad0", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": 0, "tracestate.parent_type": 0, "tracestate.parent_account_id": "33", "tracestate.sampled": true, "tracestate.priority": 1.123432, "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.ap": "2827902", "newrelic.d.tr": "6E2fEA0B173FDAD0", "newrelic.d.sa": true, "newrelic.d.pr": 1.1234321 }, "expected": [ "traceparent.parent_id", "tracestate.timestamp", "tracestate.parent_application_id", "tracestate.span_id", "tracestate.transaction_id", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id" ], "unexpected": ["newrelic.d.tk"] } ], ... ```distributed_tracing.json000066400000000000000000001421741510742411500342550ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/distributed_tracing[ { "test_name": "accept_payload", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "high_priority_but_sampled_false", "comment": "this should never happen, but is here to verify your agent only creates a span event if sampled=true, not just based off of priority", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": false }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "unexpected_events": ["Span"] }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "multiple_accept_calls", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } }, { "v": [0, 1], "d": { "ac": "33", "ap": "2097282", "tx": "23cb0b7a48407caf", "id": "b4a07f08064ee8f9", "pr": 1.234567, "sa": true, "ti": 1530549828110, "tr": "c3e4882169ac3509", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1], ["Supportability/DistributedTrace/AcceptPayload/Ignored/Multiple", 1] ] }, { "test_name": "payload_with_sampled_false", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "spans_disabled_in_parent", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74" }, "expected": ["parent.transportDuration"], "unexpected": ["parentSpanId"] }, "Span": { "expected": ["transactionId"], "unexpected": ["parentId", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "spans_disabled_in_child", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": false, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "outbound_payloads": [ { "exact": { "v": [0, 1], "d.ac": "33", "d.pr": 1.234567, "d.sa": true, "d.tr": "d6b4ba0c3a712ca", "d.ty": "App" }, "expected": ["d.ap", "d.tx", "d.ti"], "unexpected": ["d.tk", "d.id"] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": true }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1] ] }, { "test_name": "exception", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": true, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.001, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction", "TransactionError", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.001, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "error": true, "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["ErrorsByCaller/App/33/2827902/HTTP/all", 1], ["ErrorsByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "background_transaction", "trusted_account_key": "33", "account_id": "33", "web_transaction": false, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 0.234567, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 0.234567, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allOther", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allOther", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "payload_from_mobile_caller", "comment": "the transaction must be marked sampled=true so a Span event is created", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "Mobile" } } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "sampled": true }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "Mobile", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/Mobile/33/2827902/HTTP/all", 1], ["DurationByCaller/Mobile/33/2827902/HTTP/allWeb", 1], ["TransportDuration/Mobile/33/2827902/HTTP/all", 1], ["TransportDuration/Mobile/33/2827902/HTTP/allWeb", 1] ] }, { "test_name": "lowercase_known_transport_is_unknown", "comment": "beware the casing!", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "kafka", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "Unknown", "traceId": "d6b4ba0c3a712ca", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/Unknown/all", 1], ["DurationByCaller/App/33/2827902/Unknown/allWeb", 1], ["TransportDuration/App/33/2827902/Unknown/all", 1], ["TransportDuration/App/33/2827902/Unknown/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "create_payload", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "outbound_payloads": [ { "exact": { "v": [0, 1], "d.ac": "33", "d.pr": 1.234567, "d.sa": true, "d.tr": "d6b4ba0c3a712ca", "d.ty": "App" }, "expected": ["d.ap", "d.tx", "d.id", "d.ti"], "unexpected": ["d.tk"] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 1] ] }, { "test_name": "multiple_create_calls", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "id": "7d3efb1b173fecfa", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "tk": "33", "ty": "App" } } ], "outbound_payloads": [ { "exact": { "v": [0, 1], "d.ac": "33", "d.pr": 1.234567, "d.sa": true, "d.tr": "d6b4ba0c3a712ca", "d.ty": "App" }, "expected": ["d.ap", "d.tx", "d.id", "d.ti"], "unexpected": ["d.tk"] }, { "exact": { "v": [0, 1], "d.ac": "33", "d.pr": 1.234567, "d.sa": true, "d.tr": "d6b4ba0c3a712ca", "d.ty": "App" }, "expected": ["d.ap", "d.tx", "d.id", "d.ti"], "unexpected": ["d.tk"] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "d6b4ba0c3a712ca", "priority": 1.234567, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 2] ] }, { "test_name": "payload_from_trusted_partnership_account", "trusted_account_key": "44", "account_id": "11", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "tk": "44", "ty": "App" } } ], "outbound_payloads": [ { "exact": { "v": [0, 1], "d.ac": "11", "d.pr": 0.123456, "d.sa": false, "d.tr": "d6b4ba0c3a712ca", "d.tk": "44", "d.ty": "App" }, "expected": ["d.ap", "d.tx", "d.ti", "d.id"] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 1] ] }, { "test_name": "payload_has_larger_minor_version", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 2], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App", "xx": "this is an unknown field!" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "d6b4ba0c3a712ca", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "payload_with_untrusted_key", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "11", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "tk": "44", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount", 1] ] }, { "test_name": "payload_from_untrusted_account", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "44", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 0.123456, "sa": false, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount", 1] ] }, { "test_name": "payload_has_larger_major_version", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [1, 0], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/Ignored/MajorVersion", 1] ] }, { "test_name": "null_payload", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": null, "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/Ignored/Null", 1] ] }, { "test_name": "payload_missing_version", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_data", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_account", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_application", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_type", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_transactionId_or_guid", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "pr": 1.234567, "sa": true, "ti": 1518469636035, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_traceId", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "ti": 1518469636035, "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] }, { "test_name": "payload_missing_timestamp", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "major_version": 0, "minor_version": 1, "transport_type": "HTTP", "inbound_payloads": [ { "v": [0, 1], "d": { "ac": "33", "ap": "2827902", "tx": "e8b91a159289ff74", "pr": 1.234567, "sa": true, "tr": "d6b4ba0c3a712ca", "ty": "App" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/distributed_tracing/trace_context.json000066400000000000000000002356701510742411500331510ustar00rootroot00000000000000[ { "test_name": "accept_payload", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "da8bc8cc6d062849b0efcf3c169afb5a", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "payload_with_sampled_false", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-37375fc353f345b5801b166e31b76136-b4a07f08064ee8f9-00", "tracestate": "33@nr=0-0-33-2827902-b4a07f08064ee8f9-e8b91a159289ff74-0-0.123456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "37375fc353f345b5801b166e31b76136", "traceparent.trace_flags": "00", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "0", "tracestate.priority": "0.123456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "37375fc353f345b5801b166e31b76136", "priority": 0.123456, "sampled": false }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74", "parentSpanId": "b4a07f08064ee8f9" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "non_new_relic_parent", "comment": [ "If a New Relic agent started a trace, and then a non-New", "Relic tracer propagated the trace, then the traceparent span ID would", "updated by the non-New Relic tracer, but not the span ID in the New", "Relic tracestate entry" ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-7a933b0e517e8c1f6bc6a7466be6f2a0-e8b91a159289ff74-01", "tracestate": "33@nr=0-0-33-2827902-5093db371f0ba945-3ac44d37ece29bd2-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "7a933b0e517e8c1f6bc6a7466be6f2a0", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "3ac44d37ece29bd2", "parentSpanId": "e8b91a159289ff74" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "e8b91a159289ff74", "trustedParentId": "5093db371f0ba945" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "spans_disabled_in_parent", "comment": [ "If the parent is a New Relic agent with span events disabled it SHOULD omit span", "id from the tracestate. This verifies agents propogate Trace Context payloads when the", "parent is a New Relic agent with span events disabled" ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-7a933b0e517e8c1f6bc6a7466be6f2a0-e8b91a159289ff74-01", "tracestate": "33@nr=0-0-33-2827902--3ac44d37ece29bd2-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "7a933b0e517e8c1f6bc6a7466be6f2a0", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.timestamp", "tracestate.parent_application_id", "tracestate.span_id", "tracestate.transaction_id" ], "notequal": { "traceparent.parent_id": "7d3efb1b173fecfa" } } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "7a933b0e517e8c1f6bc6a7466be6f2a0", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "3ac44d37ece29bd2", "parentSpanId": "e8b91a159289ff74" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "e8b91a159289ff74" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors", "trustedParentId"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "spans_disabled_in_child", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": false, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-2c7a33d956d44531b48ec6f2e535e5c4-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "2c7a33d956d44531b48ec6f2e535e5c4", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.timestamp", "tracestate.parent_application_id", "tracestate.transaction_id" ], "notequal": { "traceparent.parent_id": "7d3efb1b173fecfa" } } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "traceId": "2c7a33d956d44531b48ec6f2e535e5c4", "priority": 1.23456, "sampled": true }, "expected": ["parent.transportDuration", "guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" } } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "spans_disabled_root", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": false, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ ], "outbound_payloads": [ { "expected": [ "traceparent.version", "traceparent.trace_id", "traceparent.parent_id", "traceparent.trace_flags", "tracestate.tenant_id", "tracestate.version", "tracestate.parent_type", "tracestate.parent_account_id", "tracestate.sampled", "tracestate.priority", "tracestate.timestamp", "tracestate.parent_application_id" ], "unexpected": [ "tracestate.span_id" ] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.type", "parent.app", "parent.account", "parent.transportDuration", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "exception", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": true, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-46ad73477e5605a5053d462bde1f95b0-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.001000-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "TransactionError", "Span"], "common":{ "exact": { "traceId": "46ad73477e5605a5053d462bde1f95b0", "priority": 1.001, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "error": true, "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["ErrorsByCaller/App/33/2827902/HTTP/all", 1], ["ErrorsByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "background_transaction", "trusted_account_key": "33", "account_id": "33", "web_transaction": false, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-7aa2a2cf20e3326bd1334b7dc7cf3ff8-7d3efb1b173fecfa-00", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "7aa2a2cf20e3326bd1334b7dc7cf3ff8", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parentId": "e8b91a159289ff74", "parent.account": "33", "parent.transportType": "HTTP", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "unexpected": ["tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allOther", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allOther", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "payload_from_mobile_caller", "comment": "the transaction must be marked sampled=true so a Span event is created", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-f6e9c09812b22fba2f1999b318ddfc8b-7d3efb1b173fecfa-00", "tracestate": "33@nr=0-2-33-2827902-7d3efb1b173fecfa----1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "f6e9c09812b22fba2f1999b318ddfc8b", "sampled": true }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "Mobile", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/Mobile/33/2827902/HTTP/all", 1], ["DurationByCaller/Mobile/33/2827902/HTTP/allWeb", 1], ["TransportDuration/Mobile/33/2827902/HTTP/all", 1], ["TransportDuration/Mobile/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "lowercase_known_transport_is_unknown", "comment": "beware the casing!", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "kafka", "inbound_headers": [ { "traceparent": "00-9cb52593888819d2b2eb63979f436b8d-f4a5ff268f63d62a-00", "tracestate": "33@nr=0-0-33-2827902-f4a5ff268f63d62a-e8b91a159289ff74-1-1.123456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "traceId": "9cb52593888819d2b2eb63979f436b8d", "priority": 1.123456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "Unknown", "parentId": "e8b91a159289ff74", "parentSpanId": "f4a5ff268f63d62a" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "f4a5ff268f63d62a", "trustedParentId": "f4a5ff268f63d62a" }, "unexpected": ["tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/Unknown/all", 1], ["DurationByCaller/App/33/2827902/Unknown/allWeb", 1], ["TransportDuration/App/33/2827902/Unknown/all", 1], ["TransportDuration/App/33/2827902/Unknown/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "create_payload", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-e22175eb1d68b6de32bf70e38458ccc3-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "e22175eb1d68b6de32bf70e38458ccc3", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "e22175eb1d68b6de32bf70e38458ccc3", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "multiple_create_calls", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-099ae207600a34ecdd5902aba9c8c6c3-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "099ae207600a34ecdd5902aba9c8c6c3", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] }, { "exact": { "traceparent.version": "00", "traceparent.trace_id": "099ae207600a34ecdd5902aba9c8c6c3", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "099ae207600a34ecdd5902aba9c8c6c3", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 2] ] }, { "test_name": "payload_from_trusted_partnership_account", "trusted_account_key": "65", "account_id": "11", "web_transaction": true, "raises_exception": false, "span_events_enabled": true, "transaction_events_enabled": true, "force_sampled_true": false, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-44673569f54fad422c3795b6cd4aef69-7d3efb1b173fecfa-00", "tracestate": "65@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "44673569f54fad422c3795b6cd4aef69", "traceparent.trace_flags": "01", "tracestate.tenant_id": "65", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "11", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "44673569f54fad422c3795b6cd4aef69", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "payload_with_untrusted_key", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-87b1c9a429205b25e5b687d890d4821f-7d3efb1b173fecfa-00", "tracestate": "44@nr=0-0-11-2827902-7d3efb1b173fecfa-e8b91a159289ff74-0-0.123456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account"] }, "Transaction": { "exact": { "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportType"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "tracingVendors": "44@nr" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "trustedParentId"] } }, "expected_metrics": [] }, { "test_name": "payload_from_untrusted_account", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-030e61f3f097890a2efcc35f22bd4f17-7d3efb1b173fecfa-00", "tracestate": "44@nr=0-0-44-2827902-7d3efb1b173fecfa-e8b91a159289ff74-0-0.123456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account"] }, "Transaction": { "exact": { "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportType"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "tracingVendors": "44@nr" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "trustedParentId"] } }, "expected_metrics": [ ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", 1], ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", 1] ] }, { "test_name": "tracestate_has_larger_version", "comment": [ "If the new relic payload's version is higher than 0, with extra new fields, all ", "the existing fields should be read and used, and the extra future feilds should ", "be ignored." ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-ccaa36c833b26ce54bafa6c4102fd740-7d3efb1b173fecfa-01", "tracestate": "33@nr=1-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035-other-new-fields" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "ccaa36c833b26ce54bafa6c4102fd740", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.timestamp", "tracestate.parent_application_id" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "ccaa36c833b26ce54bafa6c4102fd740", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "tracestate_missing_version", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01", "tracestate": "33@nr=-0-33-2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account"] }, "Transaction": { "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["parent.transportType"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "b4a146e3237b4df1" }, "unexpected": ["trustedParentId", "tracingVendors"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "tracestate_missing_account", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01", "tracestate": "33@nr=0-0--2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["guid", "traceId", "priority", "sampled", "parent.transportType"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "tracestate_missing_application", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33--7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["guid", "traceId", "priority", "sampled", "parent.transportType"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "tracestate_missing_type", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01", "tracestate": "33@nr=0--33-2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["guid", "traceId", "priority", "sampled", "parent.transportType"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "tracestate_missing_transactionId", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa--1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "5f2796876f44a3c898994ce2668e2222", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"], "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1] ] }, { "test_name": "traceparent_missing_traceId", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00--b4a146e3237b4df1-01", "tracestate": "33@nr=0-0-33-2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceParent/Parse/Exception", 1] ] }, { "test_name": "tracestate_missing_timestamp", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01", "tracestate": "33@nr=0-0-33-2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-" } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["guid", "traceId", "priority", "sampled", "parent.transportType"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "multiple_vendors_in_tracestate", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01", "tracestate": "foo=1,bar=2" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "5f2796876f44a3c898994ce2668e2222", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0" }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "tracestate.sampled", "tracestate.priority" ], "vendors": [ "foo", "bar" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "sampled": true, "traceId": "5f2796876f44a3c898994ce2668e2222" }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["parent.transportType"], "unexpected": [ "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId" ] }, "Span": { "exact": { "parentId": "b4a146e3237b4df1", "tracingVendors": "foo,bar" }, "expected": ["transactionId"], "unexpected": [ "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType" ] } }, "expected_metrics": [ ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", 1], ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", 1] ] }, { "test_name": "missing_tracestate", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-5f2796876f44a3c898994ce2668e2222-b4a146e3237b4df1-01" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "5f2796876f44a3c898994ce2668e2222", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33" }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "tracestate.sampled", "tracestate.priority" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "5f2796876f44a3c898994ce2668e2222", "sampled": true }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parentSpanId": "b4a146e3237b4df1" }, "expected": ["parent.transportType"], "unexpected": [ "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parentId" ] }, "Span": { "exact": { "parentId": "b4a146e3237b4df1" }, "expected": ["transactionId"], "unexpected": [ "parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors", "trustedParentId" ] } }, "expected_metrics": [ ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", 1], ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", 1] ] }, { "test_name": "missing_traceparent", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "tracestate": "foo=1,bar=2" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33" }, "expected": [ "traceparent.trace_id", "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "tracestate.sampled", "tracestate.priority" ], "vendors": [ ] } ], "expected_metrics": [ ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "missing_traceparent_and_tracestate", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33" }, "expected": [ "traceparent.trace_id", "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "tracestate.sampled", "tracestate.priority" ] } ], "expected_metrics": [ ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "multiple_new_relic_trace_state_entries", "trusted_account_key": "33", "account_id": "99", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-87b1c9a429205b25e5b687d890d4821f-afe162ae3117a892-00", "tracestate": "44@nr=0-0-11-30299-afe162ae3117a892-0b752e7f02c85205-0-0.123456-1518469636035,33@nr=0-0-33-2827902-7d3efb1b173fecfa-b79e301bd0ffed87-1-1.23456-1518469636025" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "87b1c9a429205b25e5b687d890d4821f", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "99", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp" ], "vendors": [ "44@nr" ] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "87b1c9a429205b25e5b687d890d4821f", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "b79e301bd0ffed87", "parentSpanId": "afe162ae3117a892" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "afe162ae3117a892", "trustedParentId": "7d3efb1b173fecfa", "tracingVendors": "44@nr" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "priority_not_converted_to_scientific_notation", "trusted_account_key": "33", "account_id": "99", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-87b1c9a429205b25e5b687d890d4821f-afe162ae3117a892-00", "tracestate": "33@nr=0-0-11-30299-afe162ae3117a892-0b752e7f02c85205-0-0.000012-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "87b1c9a429205b25e5b687d890d4821f", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "99", "tracestate.sampled": "0", "tracestate.priority": "0.000012" }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp" ] } ], "intrinsics": { "target_events": ["Transaction"], "common":{ "exact": { "traceId": "87b1c9a429205b25e5b687d890d4821f", "priority": 0.000012, "sampled": false }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "30299", "parent.account": "11", "parent.transportType": "HTTP", "parentId": "0b752e7f02c85205", "parentSpanId": "afe162ae3117a892" }, "expected": ["parent.transportDuration"] } }, "expected_metrics": [ ["DurationByCaller/App/11/30299/HTTP/all", 1], ["DurationByCaller/App/11/30299/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "w3c_and_newrelic_headers_present", "comment": "outbound newrelic headers are built from w3c headers, ignoring inbound newrelic headers", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035", "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"Mobile\",\"ac\":\"123\",\"ap\":\"51424\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"6e2fea0b173fdad0\",\"pr\":0.1234,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "da8bc8cc6d062849b0efcf3c169afb5a", "newrelic.d.pr": 1.23456, "newrelic.d.sa": true }, "expected": [ "newrelic.d.pr", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id"], "unexpected": ["newrelic.d.tk"] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "da8bc8cc6d062849b0efcf3c169afb5a", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "2827902", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1] ] }, { "test_name": "w3c_and_newrelic_headers_present_error_parsing_traceparent", "comment": [ "If the traceparent header is present on an inbound request, conforming agents MUST", "ignore any newrelic header. If the traceparent header is invalid, a new trace MUST", "be started. The newrelic header MUST be used _only_ when traceparent is _missing_." ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "garbage", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035", "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"Mobile\",\"ac\":\"33\",\"ap\":\"51424\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"6e2fea0b173fdad0\",\"pr\":0.1234,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.sa": true }, "notequal": { "newrelic.d.tr": "6e2fea0b173fdad0" }, "expected": [ "newrelic.d.pr", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id", "newrelic.d.tr" ], "unexpected": ["newrelic.d.tk"] } ], "intrinsics": { "target_events": ["Span"], "common":{ "expected": ["guid", "traceId", "priority", "sampled"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.type", "parent.app", "parent.account", "parentId", "parentSpanId", "parent.transportDuration", "tracingVendors"] }, "Span": { "expected": ["transactionId"] } }, "expected_metrics": [ ["Supportability/TraceContext/TraceParent/Parse/Exception", 1] ] }, { "test_name": "w3c_and_newrelic_headers_present_error_parsing_tracestate", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=garbage", "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"Mobile\",\"ac\":\"123\",\"ap\":\"51424\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"6e2fea0b173fdad0\",\"pr\":0.1234,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "da8bc8cc6d062849b0efcf3c169afb5a", "newrelic.d.sa": true }, "expected": [ "newrelic.d.pr", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id" ], "unexpected": ["newrelic.d.tk"] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "da8bc8cc6d062849b0efcf3c169afb5a", "sampled": true }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes", "parent.type", "parent.app", "parent.account", "parent.transportDuration", "trustedParentId"] }, "Transaction": { "exact": { "parent.transportType": "HTTP", "parentSpanId": "7d3efb1b173fecfa" }, "unexpected": ["parentId"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", 1], ["DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", 1], ["Supportability/TraceContext/TraceState/InvalidNrEntry", 1] ] }, { "test_name": "newrelic_origin_trace_id_correctly_transformed_for_w3c", "comment": [ "Tests correct handling of newrelic headers", "Agents may receive a traceId in upper-case, or shorter than 32 characters.", "In this case, the traceId MUST be left-padded with zeros AND lower-cased", "The outbound newrelic header, if configured, should include the traceId as-received." ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"App\",\"ac\":\"33\",\"ap\":\"51424\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"6E2fEA0B173FDAD0\",\"pr\":1.1234321,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "00000000000000006e2fea0b173fdad0", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.123432", "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "6E2fEA0B173FDAD0", "newrelic.d.sa": true }, "expected": [ "traceparent.parent_id", "tracestate.timestamp", "tracestate.parent_application_id", "tracestate.span_id", "tracestate.transaction_id", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id", "newrelic.d.pr" ], "unexpected": ["newrelic.d.tk"] } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "6E2fEA0B173FDAD0", "sampled": true }, "expected": ["guid", "priority"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.account": "33", "parent.app": "51424", "parent.transportType": "HTTP", "parentSpanId": "5f474d64b9cc9b2a", "parentId": "27856f70d3d314b7" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "5f474d64b9cc9b2a" }, "expected": ["transactionId"], "unexpected": ["tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/51424/HTTP/all", 1], ["DurationByCaller/App/33/51424/HTTP/allWeb", 1], ["TransportDuration/App/33/51424/HTTP/all", 1], ["TransportDuration/App/33/51424/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1] ] }, { "test_name": "span_events_enabled_transaction_events_disabled", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": true, "transaction_events_enabled": false, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-e22175eb1d68b6de32bf70e38458ccc3-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "e22175eb1d68b6de32bf70e38458ccc3", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.span_id", "tracestate.timestamp", "tracestate.parent_application_id" ], "unexpected": [ "tracestate.transaction_id" ] } ], "intrinsics": { "target_events": ["Span"], "common":{ "exact": { "traceId": "e22175eb1d68b6de32bf70e38458ccc3", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "span_events_disabled_transaction_events_disabled", "comment": [ "With both spans and transaction events disabled, there will be no ", "events to verify intrinsics against. tracestate.span_id and ", "tracestate.transaction_id are not expected on outbound payloads." ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": true, "span_events_enabled": false, "transaction_events_enabled": false, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-e22175eb1d68b6de32bf70e38458ccc3-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "e22175eb1d68b6de32bf70e38458ccc3", "traceparent.trace_flags": "01", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456" }, "expected": [ "traceparent.parent_id", "tracestate.timestamp", "tracestate.parent_application_id" ], "unexpected": [ "tracestate.span_id", "tracestate.transaction_id" ] } ], "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 1] ] }, { "test_name": "w3c_and_newrelic_headers_present_emit_both_header_types", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035", "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"Mobile\",\"ac\":\"123\",\"ap\":\"51424\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"6e2fea0b173fdad0\",\"pr\":0.1234,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "da8bc8cc6d062849b0efcf3c169afb5a", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456", "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "da8bc8cc6d062849b0efcf3c169afb5a", "newrelic.d.sa": true, "newrelic.d.pr": 1.23456 }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id" ], "unexpected": ["newrelic.d.tk"] } ], "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 1] ] }, { "test_name": "only_w3c_headers_present_emit_both_header_types", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-2827902-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "da8bc8cc6d062849b0efcf3c169afb5a", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456", "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "da8bc8cc6d062849b0efcf3c169afb5a", "newrelic.d.sa": true, "newrelic.d.pr": 1.23456 }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id" ], "unexpected": ["newrelic.d.tk"] } ], "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/TraceContext/Accept/Success", 1], ["Supportability/TraceContext/Create/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 1] ] }, { "test_name": "only_newrelic_headers_present_emit_both_header_types", "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "newrelic": "{\"v\":[0,1],\"d\":{\"ty\":\"App\",\"ac\":\"33\",\"ap\":\"2827902\",\"id\":\"5f474d64b9cc9b2a\",\"tr\":\"da8bc8cc6d062849b0efcf3c169afb5a\",\"pr\":1.23456,\"sa\":true,\"ti\":1482959525577,\"tx\":\"27856f70d3d314b7\"}}" } ], "outbound_payloads": [ { "exact": { "traceparent.version": "00", "traceparent.trace_id": "da8bc8cc6d062849b0efcf3c169afb5a", "tracestate.tenant_id": "33", "tracestate.version": "0", "tracestate.parent_type": "0", "tracestate.parent_account_id": "33", "tracestate.sampled": "1", "tracestate.priority": "1.23456", "newrelic.v": [0, 1], "newrelic.d.ty": "App", "newrelic.d.ac": "33", "newrelic.d.tr": "da8bc8cc6d062849b0efcf3c169afb5a", "newrelic.d.sa": true, "newrelic.d.pr": 1.23456 }, "expected": [ "traceparent.trace_flags", "traceparent.parent_id", "tracestate.span_id", "tracestate.transaction_id", "tracestate.parent_application_id", "tracestate.timestamp", "newrelic.d.ap", "newrelic.d.tx", "newrelic.d.ti", "newrelic.d.id" ], "unexpected": ["newrelic.d.tk"] } ], "expected_metrics": [ ["DurationByCaller/App/33/2827902/HTTP/all", 1], ["DurationByCaller/App/33/2827902/HTTP/allWeb", 1], ["TransportDuration/App/33/2827902/HTTP/all", 1], ["TransportDuration/App/33/2827902/HTTP/allWeb", 1], ["Supportability/DistributedTrace/AcceptPayload/Success", 1], ["Supportability/TraceContext/Create/Success", 1], ["Supportability/DistributedTrace/CreatePayload/Success", 1] ] }, { "test_name": "inbound_payload_from_agent_in_serverless_mode", "comment": [ "Test a payload that originates from a serverless agent. The only", "difference in the payload between a serverless and non-serverless agent", "is the `appId` in the tracestate header will be 'Unknown'." ], "trusted_account_key": "33", "account_id": "33", "web_transaction": true, "raises_exception": false, "force_sampled_true": false, "span_events_enabled": true, "transaction_events_enabled": true, "transport_type": "HTTP", "inbound_headers": [ { "traceparent": "00-da8bc8cc6d062849b0efcf3c169afb5a-7d3efb1b173fecfa-01", "tracestate": "33@nr=0-0-33-Unknown-7d3efb1b173fecfa-e8b91a159289ff74-1-1.23456-1518469636035" } ], "intrinsics": { "target_events": ["Transaction", "Span"], "common":{ "exact": { "traceId": "da8bc8cc6d062849b0efcf3c169afb5a", "priority": 1.23456, "sampled": true }, "expected": ["guid"], "unexpected": ["grandparentId", "cross_process_id", "nr.tripId", "nr.pathHash", "nr.referringPathHash", "nr.guid", "nr.referringTransactionGuid", "nr.alternatePathHashes"] }, "Transaction": { "exact": { "parent.type": "App", "parent.app": "Unknown", "parent.account": "33", "parent.transportType": "HTTP", "parentId": "e8b91a159289ff74", "parentSpanId": "7d3efb1b173fecfa" }, "expected": ["parent.transportDuration"] }, "Span": { "exact": { "parentId": "7d3efb1b173fecfa", "trustedParentId": "7d3efb1b173fecfa" }, "expected": ["transactionId"], "unexpected": ["parent.transportDuration", "parent.type", "parent.app", "parent.account", "parent.transportType", "tracingVendors"] } }, "expected_metrics": [ ["DurationByCaller/App/33/Unknown/HTTP/all", 1], ["DurationByCaller/App/33/Unknown/HTTP/allWeb", 1], ["TransportDuration/App/33/Unknown/HTTP/all", 1], ["TransportDuration/App/33/Unknown/HTTP/allWeb", 1] ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/000077500000000000000000000000001510742411500273335ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/README.md000066400000000000000000000004761510742411500306210ustar00rootroot00000000000000These tests cover parsing of Docker container IDs on Linux hosts out of `/proc/self/cgroup` (or `/proc//cgroup` more generally). The `cases.json` file lists each filename in this directory containing example `/proc/self/cgroup` content, and the expected Docker container ID that should be parsed from that file. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/cases.json000066400000000000000000000047071510742411500313340ustar00rootroot00000000000000[ { "filename": "mountinfo.txt", "containerId": "ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5", "expectedMetrics": null }, { "filename": "docker-0.9.1.txt", "containerId": "f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee", "expectedMetrics": null }, { "filename": "docker-1.0.0.txt", "containerId": "3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782", "expectedMetrics": null }, { "filename": "docker-custom-prefix.txt", "containerId": "e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7", "expectedMetrics": null }, { "filename": "docker-too-long.txt", "containerId": null, "expectedMetrics": null }, { "filename": "docker-1.1.2-lxc-driver.txt", "containerId": "cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159", "expectedMetrics": null }, { "filename": "docker-1.1.2-native-driver-fs.txt", "containerId": "2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28", "expectedMetrics": null }, { "filename": "docker-1.1.2-native-driver-systemd.txt", "containerId": "67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f", "expectedMetrics": null }, { "filename": "docker-1.3.txt", "containerId": "47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2", "expectedMetrics": null }, { "filename": "docker-gcp.txt", "containerId": "f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920", "expectedMetrics": null }, { "filename": "heroku.txt", "containerId": null, "expectedMetrics": null }, { "filename": "ubuntu-14.04-no-container.txt", "containerId": null, "expectedMetrics": null }, { "filename": "ubuntu-14.04-lxc-container.txt", "containerId": null, "expectedMetrics": null }, { "filename": "ubuntu-14.10-no-container.txt", "containerId": null, "expectedMetrics": null }, { "filename": "empty.txt", "containerId": null, "expectedMetrics": null }, { "filename": "invalid-characters.txt", "containerId": null, "expectedMetrics": null }, { "filename": "invalid-length.txt", "containerId": null, "expectedMetrics": { "Supportability/utilization/docker/error": { "callCount": 1 } } }, { "filename": "no_cpu_subsystem.txt", "containerId": null, "expectedMetrics": null } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/docker-0.9.1.txt000066400000000000000000000004071510742411500320070ustar00rootroot0000000000000011:hugetlb:/ 10:perf_event:/ 9:blkio:/ 8:freezer:/ 7:devices:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 6:memory:/ 5:cpuacct:/ 4:cpu:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 3:cpuset:/ 2:name=systemd:/go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.0.0.txt000066400000000000000000000011531510742411500317750ustar00rootroot0000000000000011:hugetlb:/ 10:perf_event:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 9:blkio:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 8:freezer:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 7:devices:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 6:memory:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 5:cpuacct:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 4:cpu:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a782 3:cpuset:/ 2:name=systemd:/ docker-1.1.2-lxc-driver.txt000066400000000000000000000014411510742411500337760ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id11:hugetlb:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 10:perf_event:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 9:blkio:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 8:freezer:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 7:name=systemd:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 6:devices:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 5:memory:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 4:cpuacct:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 3:cpu:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 2:cpuset:/lxc/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159docker-1.1.2-native-driver-fs.txt000066400000000000000000000011521510742411500351030ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id11:hugetlb:/ 10:perf_event:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 9:blkio:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 8:freezer:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 7:name=systemd:/ 6:devices:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 5:memory:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 4:cpuacct:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 3:cpu:/docker/2a4f870e24a3b52eb9fe7f3e02858c31855e213e568cfa6c76cb046ffa5b8a28 2:cpuset:/docker-1.1.2-native-driver-systemd.txt000066400000000000000000000012341510742411500361640ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id10:hugetlb:/ 9:perf_event:/ 8:blkio:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope 7:net_cls:/ 6:freezer:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope 5:devices:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope 4:memory:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope 3:cpuacct,cpu:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope 2:cpuset:/ 1:name=systemd:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scopego-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.3.txt000066400000000000000000000011301510742411500316350ustar00rootroot000000000000009:perf_event:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 8:blkio:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 7:net_cls:/ 6:freezer:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 5:devices:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 4:memory:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 3:cpuacct:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 2:cpu:/docker/47cbd16b77c50cbf71401c069cd2189f0e659af17d5a2daca3bddf59d8a870b2 1:cpuset:/ docker-custom-prefix.txt000066400000000000000000000012341510742411500340670ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id11:hugetlb:/ 10:perf_event:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 9:blkio:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 8:freezer:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 7:devices:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 6:memory:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 5:cpuacct:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 4:cpu:/custom-foobar/e6aaf072b17c345d900987ce04e37031d198b02314f8636df2c0edf6538c08c7 3:cpuset:/ 2:name=systemd:/ go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/docker-gcp.txt000066400000000000000000000013641510742411500321160ustar00rootroot0000000000000010:net_prio:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 9:perf_event:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 8:blkio:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 7:net_cls:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 6:freezer:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 5:devices:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 4:memory:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 3:cpuacct:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 2:cpu:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920 1:cpuset:/f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/docker-too-long.txt000066400000000000000000000011621510742411500330770ustar00rootroot0000000000000011:hugetlb:/ 10:perf_event:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 9:blkio:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 8:freezer:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 7:devices:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 6:memory:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 5:cpuacct:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 4:cpu:/docker/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821 3:cpuset:/ 2:name=systemd:/ go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/empty.txt000066400000000000000000000000001510742411500312200ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/heroku.txt000066400000000000000000000001571510742411500313740ustar00rootroot000000000000001:hugetlb,perf_event,blkio,freezer,devices,memory,cpuacct,cpu,cpuset:/lxc/b6d196c1-50f2-4949-abdb-5d4909864487 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/invalid-characters.txt000066400000000000000000000011301510742411500336320ustar00rootroot000000000000009:perf_event:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 8:blkio:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 7:net_cls:/ 6:freezer:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 5:devices:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 4:memory:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 3:cpuacct:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 2:cpu:/docker/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55 1:cpuset:/ go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/invalid-length.txt000066400000000000000000000003541510742411500330030ustar00rootroot000000000000009:perf_event:/docker/47cbd16b77c5 8:blkio:/docker/47cbd16b77c5 7:net_cls:/ 6:freezer:/docker/47cbd16b77c5 5:devices:/docker/47cbd16b77c5 4:memory:/docker/47cbd16b77c5 3:cpuacct:/docker/47cbd16b77c5 2:cpu:/docker/47cbd16b77c5 1:cpuset:/ go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/mountinfo.txt000066400000000000000000000050661510742411500321210ustar00rootroot00000000000000787 677 0:205 / / rw,relatime master:211 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/4LWPEJQQP4ZTPKM5BERIGWX63N:/var/lib/docker/overlay2/l/TFA7XS7THOIUG2XOSPD63ZTVCW:/var/lib/docker/overlay2/l/LU2GER2CKIEZEN3O3ZM7MMN7FE:/var/lib/docker/overlay2/l/C56BBR5IUNPYU7VFQYBD7B6TRV:/var/lib/docker/overlay2/l/3H5SWJGWS5HFV3EVBSXW3Z5EWP:/var/lib/docker/overlay2/l/FZSKODSSYVKREFDR7EOHUS4C52:/var/lib/docker/overlay2/l/X4HBP5ZZCMRNDQROSJCS3FPJXN:/var/lib/docker/overlay2/l/YXPJDSIAVYL3AQXJOMJKC2UBQ7:/var/lib/docker/overlay2/l/S3H6KC6FPHLB4YMN24TNILEUIG:/var/lib/docker/overlay2/l/Y3UADAXTZUWRMXPRAFLWZEHC4O,upperdir=/var/lib/docker/overlay2/88a9371f4db27c41c3013cea0af17d1a51d8a591a659dafe0ee4f4f5aa07ea34/diff,workdir=/var/lib/docker/overlay2/88a9371f4db27c41c3013cea0af17d1a51d8a591a659dafe0ee4f4f5aa07ea34/work 788 787 0:208 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw 789 787 0:209 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 790 789 0:210 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 791 787 0:211 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro 792 791 0:33 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw 793 789 0:207 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw 794 789 0:212 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k 795 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw 796 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw 797 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw 678 788 0:208 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw 679 788 0:208 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw 680 788 0:208 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw 681 788 0:208 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw 682 788 0:208 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw 683 788 0:209 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 684 788 0:209 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 685 788 0:209 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 686 788 0:209 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 687 791 0:213 / /sys/firmware ro,relatime - tmpfs tmpfs rogo-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id/no_cpu_subsystem.txt000066400000000000000000000013711510742411500334770ustar00rootroot0000000000000011:hugetlb:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 10:perf_event:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 9:blkio:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 8:freezer:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 7:devices:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 6:memory:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 5:cpuacct:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 4:cpu:/ 3:cpuset:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee 2:name=systemd:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee ubuntu-14.04-lxc-container.txt000066400000000000000000000003071510742411500345470ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id11:hugetlb:/lxc/p1 10:perf_event:/lxc/p1 9:blkio:/lxc/p1 8:freezer:/lxc/p1 7:devices:/lxc/p1 6:memory:/lxc/p1 5:cpuacct:/lxc/p1 4:cpu:/lxc/p1 3:cpuset:/lxc/p1 2:name=systemd:/user/1000.user/1.sessionubuntu-14.04-no-container.txt000066400000000000000000000005511510742411500343760ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id11:hugetlb:/user/1000.user/2.session 10:perf_event:/user/1000.user/2.session 9:blkio:/user/1000.user/2.session 8:freezer:/user/1000.user/2.session 7:devices:/user/1000.user/2.session 6:memory:/user/1000.user/2.session 5:cpuacct:/user/1000.user/2.session 4:cpu:/user/1000.user/2.session 3:cpuset:/user/1000.user/2.session 2:name=systemd:/user/1000.user/2.sessionubuntu-14.10-no-container.txt000066400000000000000000000002631510742411500343730ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/docker_container_id10:hugetlb:/ 9:perf_event:/ 8:blkio:/ 7:net_cls,net_prio:/ 6:freezer:/ 5:devices:/ 4:memory:/ 3:cpu,cpuacct:/ 2:cpuset:/ 1:name=systemd:/user.slice/user-1000.slice/session-2.scopego-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/labels.json000066400000000000000000000333351510742411500255120ustar00rootroot00000000000000[ { "name": "empty", "labelString": "", "warning": false, "expected": [] }, { "name": "multiple_values", "labelString": "Data Center: East;Data Center :West; Server : North;Server:South; ", "warning": false, "expected": [ { "label_type": "Data Center", "label_value": "West" }, { "label_type": "Server", "label_value": "South" } ] }, { "name": "multiple_labels_with_leading_and_trailing_whitespaces", "labelString": " Data Center : East Coast ; Deployment Flavor : Integration Environment ", "warning": false, "expected": [ { "label_type": "Data Center", "label_value": "East Coast" }, { "label_type": "Deployment Flavor", "label_value": "Integration Environment" } ] }, { "name": "single", "labelString": "Server:East", "warning": false, "expected": [ { "label_type": "Server", "label_value": "East" } ] }, { "name": "single_label_with_leading_and_trailing_whitespaces", "labelString": " Data Center : East Coast ", "warning": false, "expected": [ { "label_type": "Data Center", "label_value": "East Coast" } ] }, { "name": "single_trailing_semicolon", "labelString": "Server:East;", "warning": false, "expected": [ { "label_type": "Server", "label_value": "East" } ] }, { "name": "pair", "labelString": "Data Center:Primary;Server:East", "warning": false, "expected": [ { "label_type": "Data Center", "label_value": "Primary" }, { "label_type": "Server", "label_value": "East" } ] }, { "name": "truncation", "labelString": "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK:VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", "warning": true, "expected": [ { "label_type": "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK", "label_value": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" } ] }, { "name": "single_label_key_to_be_truncated_with_leading_and_trailing_whitespaces", "labelString": " 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345TTTTT :value", "warning": true, "expected": [ { "label_type": "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345", "label_value": "value" } ] }, { "name": "single_label_value_to_be_truncated_with_leading_and_trailing_whitespaces", "labelString": "key: 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345TTTTT ", "warning": true, "expected": [ { "label_type": "key", "label_value": "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345" } ] }, { "name": "utf8", "labelString": "kéÿ:vãlüê", "warning": false, "expected": [ { "label_type": "kéÿ", "label_value": "vãlüê" } ] }, { "name": "failed_no_delimiters", "labelString": "Server", "warning": true, "expected": [] }, { "name": "failed_no_delimiter", "labelString": "ServerNorth;", "warning": true, "expected": [] }, { "name": "failed_too_many_delimiters", "labelString": "Server:North:South;", "warning": true, "expected": [] }, { "name": "failed_no_value", "labelString": "Server: ", "warning": true, "expected": [] }, { "name": "failed_no_key", "labelString": ":North", "warning": true, "expected": [] }, { "name": "failed_no_delimiter_in_later_pair", "labelString": "Server:North;South;", "warning": true, "expected": [] }, { "name": "so_many_labels", "labelString": "0:0;1:1;2:2;3:3;4:4;5:5;6:6;7:7;8:8;9:9;10:10;11:11;12:12;13:13;14:14;15:15;16:16;17:17;18:18;19:19;20:20;21:21;22:22;23:23;24:24;25:25;26:26;27:27;28:28;29:29;30:30;31:31;32:32;33:33;34:34;35:35;36:36;37:37;38:38;39:39;40:40;41:41;42:42;43:43;44:44;45:45;46:46;47:47;48:48;49:49;50:50;51:51;52:52;53:53;54:54;55:55;56:56;57:57;58:58;59:59;60:60;61:61;62:62;63:63;64:64;65:65;66:66;67:67;68:68;69:69;70:70;71:71;72:72;73:73;74:74;75:75;76:76;77:77;78:78;79:79;80:80;81:81;82:82;83:83;84:84;85:85;86:86;87:87;88:88;89:89;90:90;91:91;92:92;93:93;94:94;95:95;96:96;97:97;98:98;99:99;", "warning": true, "expected": [ { "label_type": "0", "label_value": "0" }, { "label_type": "1", "label_value": "1" }, { "label_type": "2", "label_value": "2" }, { "label_type": "3", "label_value": "3" }, { "label_type": "4", "label_value": "4" }, { "label_type": "5", "label_value": "5" }, { "label_type": "6", "label_value": "6" }, { "label_type": "7", "label_value": "7" }, { "label_type": "8", "label_value": "8" }, { "label_type": "9", "label_value": "9" }, { "label_type": "10", "label_value": "10" }, { "label_type": "11", "label_value": "11" }, { "label_type": "12", "label_value": "12" }, { "label_type": "13", "label_value": "13" }, { "label_type": "14", "label_value": "14" }, { "label_type": "15", "label_value": "15" }, { "label_type": "16", "label_value": "16" }, { "label_type": "17", "label_value": "17" }, { "label_type": "18", "label_value": "18" }, { "label_type": "19", "label_value": "19" }, { "label_type": "20", "label_value": "20" }, { "label_type": "21", "label_value": "21" }, { "label_type": "22", "label_value": "22" }, { "label_type": "23", "label_value": "23" }, { "label_type": "24", "label_value": "24" }, { "label_type": "25", "label_value": "25" }, { "label_type": "26", "label_value": "26" }, { "label_type": "27", "label_value": "27" }, { "label_type": "28", "label_value": "28" }, { "label_type": "29", "label_value": "29" }, { "label_type": "30", "label_value": "30" }, { "label_type": "31", "label_value": "31" }, { "label_type": "32", "label_value": "32" }, { "label_type": "33", "label_value": "33" }, { "label_type": "34", "label_value": "34" }, { "label_type": "35", "label_value": "35" }, { "label_type": "36", "label_value": "36" }, { "label_type": "37", "label_value": "37" }, { "label_type": "38", "label_value": "38" }, { "label_type": "39", "label_value": "39" }, { "label_type": "40", "label_value": "40" }, { "label_type": "41", "label_value": "41" }, { "label_type": "42", "label_value": "42" }, { "label_type": "43", "label_value": "43" }, { "label_type": "44", "label_value": "44" }, { "label_type": "45", "label_value": "45" }, { "label_type": "46", "label_value": "46" }, { "label_type": "47", "label_value": "47" }, { "label_type": "48", "label_value": "48" }, { "label_type": "49", "label_value": "49" }, { "label_type": "50", "label_value": "50" }, { "label_type": "51", "label_value": "51" }, { "label_type": "52", "label_value": "52" }, { "label_type": "53", "label_value": "53" }, { "label_type": "54", "label_value": "54" }, { "label_type": "55", "label_value": "55" }, { "label_type": "56", "label_value": "56" }, { "label_type": "57", "label_value": "57" }, { "label_type": "58", "label_value": "58" }, { "label_type": "59", "label_value": "59" }, { "label_type": "60", "label_value": "60" }, { "label_type": "61", "label_value": "61" }, { "label_type": "62", "label_value": "62" }, { "label_type": "63", "label_value": "63" } ] }, { "name": "trailing_semicolons", "labelString": "foo:bar;;", "warning": false, "expected": [ { "label_type": "foo", "label_value": "bar" } ] }, { "name": "leading_semicolons", "labelString": ";;foo:bar", "warning": false, "expected": [ { "label_type": "foo", "label_value": "bar" } ] }, { "name": "empty_label", "labelString": "foo:bar;;zip:zap", "warning": true, "expected": [] }, { "name": "trailing_colons", "labelString": "foo:bar;:", "warning": true, "expected": [] }, { "name": "leading_colons", "labelString": ":;foo:bar", "warning": true, "expected": [] }, { "name": "empty_pair", "labelString": " : ", "warning": true, "expected": [] }, { "name": "empty_pair_in_middle_of_string", "labelString": "foo:bar; : ;zip:zap", "warning": true, "expected": [] }, { "name": "long_multibyte_utf8", "labelString": "foo:€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€", "warning": true, "expected": [ { "label_type": "foo", "label_value": "€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€" } ] }, { "name": "long_4byte_utf8", "labelString": "foo:𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆", "warning": true, "expected": [ { "label_type": "foo", "label_value": "𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆"}] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/lambda/000077500000000000000000000000001510742411500245665ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/lambda/README.md000066400000000000000000000014131510742411500260440ustar00rootroot00000000000000# event_source_info This test fixture is intended to verify that the the agent correctly detects event types (in languages where type is dynamic) and correctly harvests ARN values. The fixtures are structured as follows: { "": { "expected_type": "alb", "expected_arn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a", "event": {...} } } Each fixture is a JSON object with three keys: `expected_type`, `expected_arn`, and `event`. `event` is an example AWS Lambda invocation event. The other two are values that the agent should extract from that event, and ought to write test assertions against. The top-level `` is provided only for convenience. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/lambda/event_source_info.json000066400000000000000000000436561510742411500312130ustar00rootroot00000000000000{ "alb": { "expected_type": "alb", "expected_arn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a", "event": { "requestContext": { "elb": { "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" } }, "httpMethod": "GET", "path": "/lambda", "queryStringParameters": { "query": "1234ABCD" }, "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", "connection": "keep-alive", "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", "x-forwarded-for": "72.12.164.125", "x-forwarded-port": "80", "x-forwarded-proto": "http", "x-imforwards": "20" }, "body": "", "isBase64Encoded": false } }, "apiGateway": { "expected_type": "apiGateway", "expected_arn": null, "event": { "body": "eyJ0ZXN0IjoiYm9keSJ9", "resource": "/{proxy+}", "path": "/path/to/resource", "httpMethod": "POST", "isBase64Encoded": true, "queryStringParameters": { "foo": "bar" }, "multiValueQueryStringParameters": { "foo": [ "bar" ] }, "pathParameters": { "proxy": "/path/to/resource" }, "stageVariables": { "baz": "qux" }, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, sdch", "Accept-Language": "en-US,en;q=0.8", "Cache-Control": "max-age=0", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "US", "Host": "1234567890.execute-api.us-west-2.amazonaws.com", "Upgrade-Insecure-Requests": "1", "User-Agent": "Custom User Agent String", "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "multiValueHeaders": { "Accept": [ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ], "Accept-Encoding": [ "gzip, deflate, sdch" ], "Accept-Language": [ "en-US,en;q=0.8" ], "Cache-Control": [ "max-age=0" ], "CloudFront-Forwarded-Proto": [ "https" ], "CloudFront-Is-Desktop-Viewer": [ "true" ], "CloudFront-Is-Mobile-Viewer": [ "false" ], "CloudFront-Is-SmartTV-Viewer": [ "false" ], "CloudFront-Is-Tablet-Viewer": [ "false" ], "CloudFront-Viewer-Country": [ "US" ], "Host": [ "0123456789.execute-api.us-west-2.amazonaws.com" ], "Upgrade-Insecure-Requests": [ "1" ], "User-Agent": [ "Custom User Agent String" ], "Via": [ "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" ], "X-Amz-Cf-Id": [ "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" ], "X-Forwarded-For": [ "127.0.0.1, 127.0.0.2" ], "X-Forwarded-Port": [ "443" ], "X-Forwarded-Proto": [ "https" ] }, "requestContext": { "accountId": "123456789012", "resourceId": "123456", "stage": "prod", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "requestTime": "09/Apr/2015:12:34:56 +0000", "requestTimeEpoch": 1428582896000, "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "accessKey": null, "sourceIp": "127.0.0.1", "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Custom User Agent String", "user": null }, "path": "/prod/path/to/resource", "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "1234567890", "protocol": "HTTP/1.1" } } }, "cloudFront": { "expected_type": "cloudFront", "expected_arn": null, "event": { "Records": [ { "cf": { "config": { "distributionId": "EXAMPLE" }, "request": { "uri": "/test", "querystring": "auth=test&foo=bar", "method": "GET", "clientIp": "2001:cdba::3257:9652", "headers": { "host": [ { "key": "Host", "value": "d123.cf.net" } ], "user-agent": [ { "key": "User-Agent", "value": "Test Agent" } ], "user-name": [ { "key": "User-Name", "value": "aws-cloudfront" } ] } } } } ] } }, "cloudWatch_scheduled": { "expected_type": "cloudWatch_scheduled", "expected_arn": "arn:aws:events:us-west-2:123456789012:rule/ExampleRule", "event": { "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", "detail-type": "Scheduled Event", "source": "aws.events", "account": "{{{account-id}}}", "time": "1970-01-01T00:00:00Z", "region": "us-west-2", "resources": [ "arn:aws:events:us-west-2:123456789012:rule/ExampleRule" ], "detail": {} } }, "dynamo_streams": { "expected_type": "dynamo_streams", "expected_arn": "arn:aws:dynamodb:us-west-2:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", "event": { "Records": [ { "eventID": "c4ca4238a0b923820dcc509a6f75849b", "eventName": "INSERT", "eventVersion": "1.1", "eventSource": "aws:dynamodb", "awsRegion": "us-west-2", "dynamodb": { "Keys": { "Id": { "N": "101" } }, "NewImage": { "Message": { "S": "New item!" }, "Id": { "N": "101" } }, "ApproximateCreationDateTime": 1428537600, "SequenceNumber": "4421584500000000017450439091", "SizeBytes": 26, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb:us-west-2:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" }, { "eventID": "c81e728d9d4c2f636f067f89cc14862c", "eventName": "MODIFY", "eventVersion": "1.1", "eventSource": "aws:dynamodb", "awsRegion": "us-west-2", "dynamodb": { "Keys": { "Id": { "N": "101" } }, "NewImage": { "Message": { "S": "This item has changed" }, "Id": { "N": "101" } }, "OldImage": { "Message": { "S": "New item!" }, "Id": { "N": "101" } }, "ApproximateCreationDateTime": 1428537600, "SequenceNumber": "4421584500000000017450439092", "SizeBytes": 59, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb:us-west-2:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" }, { "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", "eventName": "REMOVE", "eventVersion": "1.1", "eventSource": "aws:dynamodb", "awsRegion": "us-west-2", "dynamodb": { "Keys": { "Id": { "N": "101" } }, "OldImage": { "Message": { "S": "This item has changed" }, "Id": { "N": "101" } }, "ApproximateCreationDateTime": 1428537600, "SequenceNumber": "4421584500000000017450439093", "SizeBytes": 38, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb:us-west-2:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" } ] } }, "firehose": { "expected_type": "firehose", "expected_arn": "arn:aws:kinesis:EXAMPLE", "event": { "invocationId": "invocationIdExample", "deliveryStreamArn": "arn:aws:kinesis:EXAMPLE", "region": "us-west-2", "records": [ { "recordId": "49546986683135544286507457936321625675700192471156785154", "approximateArrivalTimestamp": 1495072949453, "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", "kinesisRecordMetadata": { "shardId": "shardId-000000000000", "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", "approximateArrivalTimestamp": "2012-04-23T18:25:43.511Z", "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", "subsequenceNumber": "" } } ] } }, "kinesis": { "expected_type": "kinesis", "expected_arn": "arn:aws:kinesis:EXAMPLE", "event": { "Records": [ { "kinesis": { "partitionKey": "partitionKey-03", "kinesisSchemaVersion": "1.0", "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", "approximateArrivalTimestamp": 1428537600 }, "eventSource": "aws:kinesis", "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", "invokeIdentityArn": "arn:aws:iam::EXAMPLE", "eventVersion": "1.0", "eventName": "aws:kinesis:record", "eventSourceARN": "arn:aws:kinesis:EXAMPLE", "awsRegion": "us-west-2" } ] } }, "s3": { "expected_type": "s3", "expected_arn": "arn:aws:s3:::example-bucket", "event": { "Records": [ { "eventVersion": "2.0", "eventSource": "aws:s3", "awsRegion": "us-west-2", "eventTime": "1970-01-01T00:00:00.000Z", "eventName": "ObjectCreated:Put", "userIdentity": { "principalId": "EXAMPLE" }, "requestParameters": { "sourceIPAddress": "127.0.0.1" }, "responseElements": { "x-amz-request-id": "EXAMPLE123456789", "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" }, "s3": { "s3SchemaVersion": "1.0", "configurationId": "testConfigRule", "bucket": { "name": "example-bucket", "ownerIdentity": { "principalId": "EXAMPLE" }, "arn": "arn:aws:s3:::example-bucket" }, "object": { "key": "test/key", "size": 1024, "eTag": "0123456789abcdef0123456789abcdef", "sequencer": "0A1B2C3D4E5F678901" } } } ] } }, "ses": { "expected_type": "ses", "expected_arn": null, "event": { "Records": [ { "eventSource": "aws:ses", "eventVersion": "1.0", "ses": { "mail": { "commonHeaders": { "date": "Wed, 7 Oct 2015 12:34:56 -0700", "from": [ "Jane Doe " ], "messageId": "<0123456789example.com>", "returnPath": "janedoe@example.com", "subject": "Test Subject", "to": [ "johndoe@example.com" ] }, "destination": [ "johndoe@example.com" ], "headers": [ { "name": "Return-Path", "value": "" }, { "name": "Received", "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.us-west-2.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" }, { "name": "DKIM-Signature", "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=example; h=mime-version:from:date:message-id:subject:to:content-type; bh=jX3F0bCAI7sIbkHyy3mLYO28ieDQz2R0P8HwQkklFj4=; b=sQwJ+LMe9RjkesGu+vqU56asvMhrLRRYrWCbVt6WJulueecwfEwRf9JVWgkBTKiL6m2hr70xDbPWDhtLdLO+jB3hzjVnXwK3pYIOHw3vxG6NtJ6o61XSUwjEsp9tdyxQjZf2HNYee873832l3K1EeSXKzxYk9Pwqcpi3dMC74ct9GukjIevf1H46hm1L2d9VYTL0LGZGHOAyMnHmEGB8ZExWbI+k6khpurTQQ4sp4PZPRlgHtnj3Zzv7nmpTo7dtPG5z5S9J+L+Ba7dixT0jn3HuhaJ9b+VThboo4YfsX9PMNhWWxGjVksSFOcGluPO7QutCPyoY4gbxtwkN9W69HA==" }, { "name": "MIME-Version", "value": "1.0" }, { "name": "From", "value": "Jane Doe " }, { "name": "Date", "value": "Wed, 7 Oct 2015 12:34:56 -0700" }, { "name": "Message-ID", "value": "<0123456789example.com>" }, { "name": "Subject", "value": "Test Subject" }, { "name": "To", "value": "johndoe@example.com" }, { "name": "Content-Type", "value": "text/plain; charset=UTF-8" } ], "headersTruncated": false, "messageId": "o3vrnil0e2ic28trm7dfhrc2v0clambda4nbp0g1", "source": "janedoe@example.com", "timestamp": "1970-01-01T00:00:00.000Z" }, "receipt": { "action": { "functionArn": "arn:aws:lambda:us-west-2:123456789012:function:Example", "invocationType": "Event", "type": "Lambda" }, "dkimVerdict": { "status": "PASS" }, "processingTimeMillis": 574, "recipients": [ "johndoe@example.com" ], "spamVerdict": { "status": "PASS" }, "spfVerdict": { "status": "PASS" }, "timestamp": "1970-01-01T00:00:00.000Z", "virusVerdict": { "status": "PASS" } } } } ] } }, "sns": { "expected_type": "sns", "expected_arn": "arn:aws:sns:us-west-2:{{{accountId}}}:ExampleTopic", "event": { "Records": [ { "EventSource": "aws:sns", "EventVersion": "1.0", "EventSubscriptionArn": "arn:aws:sns:us-west-2:{{{accountId}}}:ExampleTopic", "Sns": { "Type": "Notification", "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", "TopicArn": "arn:aws:sns:us-west-2:123456789012:ExampleTopic", "Subject": "example subject", "Message": "example message", "Timestamp": "1970-01-01T00:00:00.000Z", "SignatureVersion": "1", "Signature": "EXAMPLE", "SigningCertUrl": "EXAMPLE", "UnsubscribeUrl": "EXAMPLE", "MessageAttributes": { "Test": { "Type": "String", "Value": "TestString" }, "TestBinary": { "Type": "Binary", "Value": "TestBinary" } } } } ] } }, "sqs": { "expected_type": "sqs", "expected_arn": "arn:aws:sqs:us-west-2:123456789012:MyQueue", "event": { "Records": [ { "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", "receiptHandle": "MessageReceiptHandle", "body": "Hello from SQS!", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1523232000000", "SenderId": "123456789012", "ApproximateFirstReceiveTimestamp": "1523232000001" }, "messageAttributes": {}, "md5OfBody": "7b270e59b47ff90a553787216d55d91d", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:MyQueue", "awsRegion": "us-west-2" } ] } } } go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/language_agents_security_policies.json000066400000000000000000000306201510742411500332040ustar00rootroot00000000000000[{ "name": "should respect record_sql policy", "required_features": ["record_sql"], "starting_policy_settings": { "record_sql": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": false, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "record_sql": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "record_sql": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect attributes_include policy", "required_features": ["attributes_include"], "starting_policy_settings": { "attributes_include": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": false, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "attributes_include": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "attributes_include": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect allow_raw_exception_messages policy and more secure local setting", "required_features": ["allow_raw_exception_messages"], "starting_policy_settings": { "allow_raw_exception_messages": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": false, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "allow_raw_exception_messages": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "allow_raw_exception_messages": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect custom_events policy", "required_features": ["custom_events"], "starting_policy_settings": { "custom_events": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": false, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "custom_events": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "custom_events": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect custom_parameters policy", "required_features": ["custom_parameters"], "starting_policy_settings": { "custom_parameters": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": false, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "custom_parameters": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "custom_parameters": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect custom_instrumentation_editor policy", "required_features": ["custom_instrumentation_editor"], "starting_policy_settings": { "custom_instrumentation_editor": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": false, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "custom_instrumentation_editor": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "custom_instrumentation_editor": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect message_parameters policy", "required_features": ["message_parameters"], "starting_policy_settings": { "message_parameters": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": false, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7} }, "expected_connect_policies": { "message_parameters": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "message_parameters": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should respect job_arguments policy", "required_features": ["job_arguments"], "starting_policy_settings": { "job_arguments": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": true, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": false, "required": false, "position": 7} }, "expected_connect_policies": { "job_arguments": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "job_arguments": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should fail because the agent knows about a policy the server does not", "required_features": ["record_sql"], "starting_policy_settings": { "record_sql": {"enabled": true} }, "security_policies": {}, "expected_connect_policies": {}, "validate_policies_not_in_connect": [], "ending_policy_settings": { "record_sql": {"enabled": true} }, "should_log": true, "should_shutdown": true }, { "name": "should not respond with unknown policies", "required_features": ["record_sql"], "starting_policy_settings": { "record_sql": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": false, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7}, "some_new_feature": {"enabled": false, "required": false, "position": 8} }, "expected_connect_policies": { "record_sql": {"enabled": false} }, "validate_policies_not_in_connect": [ "some_new_feature" ], "ending_policy_settings": { "record_sql": {"enabled": false} }, "should_log": false, "should_shutdown": false }, { "name": "should shutdown for required but unknown policies", "required_features": ["record_sql"], "starting_policy_settings": { "record_sql": {"enabled": true} }, "security_policies": { "record_sql": {"enabled": false, "required": false, "position": 0}, "attributes_include": {"enabled": true, "required": false, "position": 1}, "allow_raw_exception_messages": {"enabled": true, "required": false, "position": 2}, "custom_events": {"enabled": true, "required": false, "position": 3}, "custom_parameters": {"enabled": true, "required": false, "position": 4}, "custom_instrumentation_editor": {"enabled": true, "required": false, "position": 5}, "message_parameters": {"enabled": true, "required": false, "position": 6}, "job_arguments": {"enabled": true, "required": false, "position": 7}, "some_new_feature": {"enabled": false, "required": true, "position": 8} }, "expected_connect_policies": { "record_sql": {"enabled": false} }, "validate_policies_not_in_connect": [], "ending_policy_settings": { "record_sql": {"enabled": false} }, "should_log": true, "should_shutdown": true }] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/000077500000000000000000000000001510742411500313305ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/README.md000066400000000000000000000016521510742411500326130ustar00rootroot00000000000000# PostgreSQL explain plan obfuscation tests These tests show how explain plans for PostgreSQL should be obfuscated when SQL obfuscation is enabled. Obfuscation of explain plans for PostgreSQL is necessary because they can include portions of the original query that may contain sensitive data. Each test case consists of a set of files with the following extensions: * `.query.txt` - the original SQL query that is being explained * `.explain.txt` - the raw un-obfuscated output from running `EXPLAIN ` * `.colon_obfuscated.txt` - the desired obfuscated explain output if using the default, more aggressive obfuscation strategy described [here](https://newrelic.atlassian.net/wiki/display/eng/Obfuscating+PostgreSQL+Explain+plans). * `.obfuscated.txt` - the desired obfuscated explain output if using a more accurate, less aggressive obfuscation strategy detailed in this [Jive thread](https://newrelic.jiveon.com/thread/1851). basic_where.colon_obfuscated.txt000066400000000000000000000001471510742411500375770ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: ? Filter: ?basic_where.explain.txt000066400000000000000000000002301510742411500357170ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = 1234) Filter: ((title)::text = 'sensitive text'::text)basic_where.obfuscated.txt000066400000000000000000000002061510742411500364010ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = ?) Filter: ((title)::text = ?::text)basic_where.query.txt000066400000000000000000000001101510742411500354210ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.id=1234 AND blogs.title='sensitive text'current_date.colon_obfuscated.txt000066400000000000000000000001171510742411500400000ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) Filter: ? current_date.explain.txt000066400000000000000000000002021510742411500361220ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) Filter: ((j = 'a'::"char") AND (k = ('now'::cstring)::date)) current_date.obfuscated.txt000066400000000000000000000001741510742411500366110ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) Filter: ((j = ?::"char") AND (k = (?::cstring)::date)) current_date.query.txt000066400000000000000000000001201510742411500356260ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationexplain select * from explain_plan_test_4 where j = 'abcd' and k = current_date date.colon_obfuscated.txt000066400000000000000000000001201510742411500362300ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) Filter: ? date.explain.txt000066400000000000000000000001471510742411500343700ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) Filter: (k = '2001-09-28'::date) date.obfuscated.txt000066400000000000000000000001341510742411500350430ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) Filter: (k = ?::date) go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.query.txt000066400000000000000000000001071510742411500341500ustar00rootroot00000000000000explain select * from explain_plan_test_4 where k = date '2001-09-28'" embedded_newline.colon_obfuscated.txt000066400000000000000000000001021510742411500405650ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on blogs (cost=0.00..1.01 rows=1 width=540) Filter: ? embedded_newline.explain.txt000066400000000000000000000001571510742411500367260ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on blogs (cost=0.00..1.01 rows=1 width=540) Filter: ((title)::text = '\x08\x0C \r '::text) embedded_newline.obfuscated.txt000066400000000000000000000001321510742411500373760ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on blogs (cost=0.00..1.01 rows=1 width=540) Filter: ((title)::text = ?::text) embedded_newline.query.txt000066400000000000000000000000641510742411500364300ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationselect * from blogs where title = E'\x08\x0c\n\r\t' embedded_quote.colon_obfuscated.txt000066400000000000000000000001201510742411500402610ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) Filter: ? embedded_quote.explain.txt000066400000000000000000000001511510742411500364140ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) Filter: (c = 'three''three'::text) embedded_quote.obfuscated.txt000066400000000000000000000001341510742411500370740ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) Filter: (c = ?::text) embedded_quote.query.txt000066400000000000000000000001031510742411500361160ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationexplain select * from explain_plan_test_1 where c = 'three''three' floating_point.colon_obfuscated.txt000066400000000000000000000001201510742411500403270ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) Filter: ? floating_point.explain.txt000066400000000000000000000001641510742411500364660ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) Filter: ((a)::numeric = 10000000000::numeric) floating_point.obfuscated.txt000066400000000000000000000001521510742411500371420ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) Filter: ((a)::numeric = ?::numeric) floating_point.query.txt000066400000000000000000000000711510742411500361700ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationexplain select * from explain_plan_test_1 where a = 1e10 function_with_strings.colon_obfuscated.txt000066400000000000000000000003751510742411500417600ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Hash Join (cost=12.93..26.33 rows=130 width=1113) Hash Cond: ? -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=540) -> Hash (cost=11.30..11.30 rows=130 width=573) -> Seq Scan on posts (cost=0.00..11.30 rows=130 width=573)function_with_strings.explain.txt000066400000000000000000000004751510742411500401100ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Hash Join (cost=12.93..26.33 rows=130 width=1113) Hash Cond: (pg_catalog.concat(blogs.title, '-suffix') = (posts.title)::text) -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=540) -> Hash (cost=11.30..11.30 rows=130 width=573) -> Seq Scan on posts (cost=0.00..11.30 rows=130 width=573)function_with_strings.obfuscated.txt000066400000000000000000000004651510742411500405660ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Hash Join (cost=12.93..26.33 rows=130 width=1113) Hash Cond: (pg_catalog.concat(blogs.title, ?) = (posts.title)::text) -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=540) -> Hash (cost=11.30..11.30 rows=130 width=573) -> Seq Scan on posts (cost=0.00..11.30 rows=130 width=573)function_with_strings.query.txt000066400000000000000000000001141510742411500376030ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs JOIN posts ON posts.title=CONCAT(blogs.title, '-suffix')quote_in_table_name.colon_obfuscated.txt000066400000000000000000000001231510742411500413100ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) Filter: ? quote_in_table_name.explain.txt000066400000000000000000000001461510742411500374440ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) Filter: (i = '"abcd"'::text) quote_in_table_name.obfuscated.txt000066400000000000000000000001371510742411500401230ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSeq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) Filter: (i = ?::text) quote_in_table_name.query.txt000066400000000000000000000001001510742411500371370ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationexplain select * from "explain_plan_test'_3" where i = '"abcd"' subplan.colon_obfuscated.txt000066400000000000000000000004121510742411500367630ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationInsert on explain_plan_test_1 (cost=24.50..49.00 rows=580 width=40) -> Seq Scan on explain_plan_test_2 (cost=24.50..49.00 rows=580 width=40) Filter: ? SubPlan 1 -> Seq Scan on explain_plan_test_1 (cost=0.00..21.60 rows=1160 width=4) subplan.explain.txt000066400000000000000000000004411510742411500351140ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationInsert on explain_plan_test_1 (cost=24.50..49.00 rows=580 width=40) -> Seq Scan on explain_plan_test_2 (cost=24.50..49.00 rows=580 width=40) Filter: (NOT (hashed SubPlan 1)) SubPlan 1 -> Seq Scan on explain_plan_test_1 (cost=0.00..21.60 rows=1160 width=4) subplan.obfuscated.txt000066400000000000000000000004411510742411500355730ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationInsert on explain_plan_test_1 (cost=24.50..49.00 rows=580 width=40) -> Seq Scan on explain_plan_test_2 (cost=24.50..49.00 rows=580 width=40) Filter: (NOT (hashed SubPlan 1)) SubPlan 1 -> Seq Scan on explain_plan_test_1 (cost=0.00..21.60 rows=1160 width=4) subplan.query.txt000066400000000000000000000002211510742411500346150ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationexplain insert into explain_plan_test_1 select * from explain_plan_test_2 where explain_plan_test_2.d not in (select a from explain_plan_test_1) where_with_integer.colon_obfuscated.txt000066400000000000000000000001331510742411500412010ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: ? where_with_integer.explain.txt000066400000000000000000000001451510742411500373330ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = 1234) where_with_integer.obfuscated.txt000066400000000000000000000001421510742411500400070ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = ?) where_with_integer.query.txt000066400000000000000000000000501510742411500370330ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.id=1234 where_with_regex_chars.colon_obfuscated.txt000066400000000000000000000001041510742411500420340ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?where_with_regex_chars.explain.txt000066400000000000000000000001441510742411500401670ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = '][^|)/('::text)where_with_regex_chars.obfuscated.txt000066400000000000000000000001341510742411500406450ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)where_with_regex_chars.query.txt000066400000000000000000000000571510742411500376770ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title='][^|)/('where_with_substring.colon_obfuscated.txt000066400000000000000000000001471510742411500415710ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: ? Filter: ?where_with_substring.explain.txt000066400000000000000000000002171510742411500377160ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = 15402) Filter: ((title)::text = 'logs'::text)where_with_substring.obfuscated.txt000066400000000000000000000002061510742411500403730ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) Index Cond: (id = ?) Filter: ((title)::text = ?::text)where_with_substring.query.txt000066400000000000000000000000771510742411500374270ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.id=15402 AND blogs.title='logs'with_escape_case1.colon_obfuscated.txt000066400000000000000000000001041510742411500406640ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case1.explain.txt000066400000000000000000000001451510742411500370200ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'foo''bar'::text)with_escape_case1.obfuscated.txt000066400000000000000000000001341510742411500374750ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case1.query.txt000066400000000000000000000000711510742411500365230ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationEXPLAIN SELECT * FROM blogs WHERE blogs.title=E'foo\'bar'with_escape_case2.colon_obfuscated.txt000066400000000000000000000001041510742411500406650ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case2.explain.txt000066400000000000000000000001571510742411500370240ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = '\x08\x0C \r '::text)with_escape_case2.obfuscated.txt000066400000000000000000000001341510742411500374760ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case2.query.txt000066400000000000000000000000631510742411500365250ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=E'\b\f\n\r\t'with_escape_case3.colon_obfuscated.txt000066400000000000000000000001041510742411500406660ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case3.explain.txt000066400000000000000000000001461510742411500370230ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = '\x01\x079'::text)with_escape_case3.obfuscated.txt000066400000000000000000000001341510742411500374770ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case3.query.txt000066400000000000000000000000571510742411500365310ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=E'\1\7\9'with_escape_case4.colon_obfuscated.txt000066400000000000000000000001041510742411500406670ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case4.explain.txt000066400000000000000000000001451510742411500370230ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'foo''bar'::text)with_escape_case4.obfuscated.txt000066400000000000000000000001341510742411500375000ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case4.query.txt000066400000000000000000000000601510742411500365240ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title='foo\'bar'with_escape_case5.colon_obfuscated.txt000066400000000000000000000001041510742411500406700ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case5.explain.txt000066400000000000000000000001361510742411500370240ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'U'::text)with_escape_case5.obfuscated.txt000066400000000000000000000001341510742411500375010ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case5.query.txt000066400000000000000000000000551510742411500365310ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=E'\x55'with_escape_case6.colon_obfuscated.txt000066400000000000000000000001041510742411500406710ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case6.explain.txt000066400000000000000000000001411510742411500370210ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'data'::text)with_escape_case6.obfuscated.txt000066400000000000000000000001341510742411500375020ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case6.query.txt000066400000000000000000000000731510742411500365320ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=E'd\u0061t\U00000061'with_escape_case7.colon_obfuscated.txt000066400000000000000000000001041510742411500406720ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case7.explain.txt000066400000000000000000000001411510742411500370220ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'data'::text)with_escape_case7.obfuscated.txt000066400000000000000000000001341510742411500375030ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case7.query.txt000066400000000000000000000000711510742411500365310ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=U&'d\0061t\+000061'with_escape_case8.colon_obfuscated.txt000066400000000000000000000001041510742411500406730ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case8.explain.txt000066400000000000000000000001451510742411500370270ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'слон'::text)with_escape_case8.obfuscated.txt000066400000000000000000000001341510742411500375040ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case8.query.txt000066400000000000000000000000761510742411500365370ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=U&'\0441\043B\043E\043D'with_escape_case9.colon_obfuscated.txt000066400000000000000000000001041510742411500406740ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ?with_escape_case9.explain.txt000066400000000000000000000001411510742411500370240ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = 'data'::text)with_escape_case9.obfuscated.txt000066400000000000000000000001341510742411500375050ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) Filter: ((title)::text = ?::text)with_escape_case9.query.txt000066400000000000000000000001051510742411500365310ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/postgres_explain_obfuscationSELECT * FROM blogs WHERE blogs.title=U&'d!0061t!+000061' UESCAPE '!'go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/000077500000000000000000000000001510742411500260345ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_1logical.txt000066400000000000000000000001121510742411500324320ustar00rootroot00000000000000processor : 0 model name : AMD Duron(tm) processor cache size : 64 KB go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_2logical.txt000066400000000000000000000004541510742411500324440ustar00rootroot00000000000000processor : 0 model name : Intel(R) Pentium(R) 4 CPU 2.80GHz cache size : 1024 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 1 processor : 1 model name : Intel(R) Pentium(R) 4 CPU 2.80GHz cache size : 1024 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 1 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_2core_2logical.txt000066400000000000000000000004541510742411500324450ustar00rootroot00000000000000processor : 0 model name : Intel(R) Pentium(R) D CPU 3.00GHz cache size : 2048 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 2 processor : 1 model name : Intel(R) Pentium(R) D CPU 3.00GHz cache size : 2048 KB physical id : 0 siblings : 2 core id : 1 cpu cores : 2 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_4core_4logical.txt000066400000000000000000000012201510742411500324410ustar00rootroot00000000000000processor : 0 model name : Intel(R) Xeon(R) CPU E5410 @ 2.33GHz cache size : 6144 KB physical id : 0 siblings : 4 core id : 0 cpu cores : 4 processor : 1 model name : Intel(R) Xeon(R) CPU E5410 @ 2.33GHz cache size : 6144 KB physical id : 0 siblings : 4 core id : 1 cpu cores : 4 processor : 2 model name : Intel(R) Xeon(R) CPU E5410 @ 2.33GHz cache size : 6144 KB physical id : 0 siblings : 4 core id : 2 cpu cores : 4 processor : 3 model name : Intel(R) Xeon(R) CPU E5410 @ 2.33GHz cache size : 6144 KB physical id : 0 siblings : 4 core id : 3 cpu cores : 4 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_12core_24logical.txt000066400000000000000000000412031510742411500326100ustar00rootroot00000000000000processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 0 cpu cores : 6 apicid : 32 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.18 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 0 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 2 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 1 cpu cores : 6 apicid : 34 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 3 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 2 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 4 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 2 cpu cores : 6 apicid : 36 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5319.95 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 5 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 4 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.06 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 6 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 8 cpu cores : 6 apicid : 48 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 7 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 8 cpu cores : 6 apicid : 16 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5319.98 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 8 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 9 cpu cores : 6 apicid : 50 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 9 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 9 cpu cores : 6 apicid : 18 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5319.99 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 10 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 10 cpu cores : 6 apicid : 52 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 11 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 10 cpu cores : 6 apicid : 20 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 12 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 0 cpu cores : 6 apicid : 33 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5319.94 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 13 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 1 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 14 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 1 cpu cores : 6 apicid : 35 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 15 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 3 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 16 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 2 cpu cores : 6 apicid : 37 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 17 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 5 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 18 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 8 cpu cores : 6 apicid : 49 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5330.86 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 19 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 8 cpu cores : 6 apicid : 17 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 20 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 9 cpu cores : 6 apicid : 51 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 21 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 9 cpu cores : 6 apicid : 19 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.01 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 22 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 1 siblings : 12 core id : 10 cpu cores : 6 apicid : 53 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.05 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] processor : 23 vendor_id : GenuineIntel cpu family : 6 model : 44 model name : Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping : 2 cpu MHz : 2660.090 cache size : 12288 KB physical id : 0 siblings : 12 core id : 10 cpu cores : 6 apicid : 21 fpu : yes fpu_exception : yes cpuid level : 11 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx pdpe1gb rdtscp lm constant_tsc ida nonstop_tsc arat pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr sse4_1 sse4_2 popcnt lahf_lm bogomips : 5320.02 clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: [8] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_20core_40logical.txt000066400000000000000000001127051510742411500326130ustar00rootroot00000000000000processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 0 cpu cores : 10 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 1 cpu cores : 10 apicid : 2 initial apicid : 2 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 2 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 2 cpu cores : 10 apicid : 4 initial apicid : 4 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 3 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 3 cpu cores : 10 apicid : 6 initial apicid : 6 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 4 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 4 cpu cores : 10 apicid : 8 initial apicid : 8 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 5 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 8 cpu cores : 10 apicid : 16 initial apicid : 16 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 6 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 9 cpu cores : 10 apicid : 18 initial apicid : 18 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 7 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 10 cpu cores : 10 apicid : 20 initial apicid : 20 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 8 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 11 cpu cores : 10 apicid : 22 initial apicid : 22 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 9 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 12 cpu cores : 10 apicid : 24 initial apicid : 24 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 10 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 0 cpu cores : 10 apicid : 32 initial apicid : 32 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 11 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 1 cpu cores : 10 apicid : 34 initial apicid : 34 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 12 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 2 cpu cores : 10 apicid : 36 initial apicid : 36 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 13 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 3 cpu cores : 10 apicid : 38 initial apicid : 38 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 14 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 4 cpu cores : 10 apicid : 40 initial apicid : 40 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 15 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 8 cpu cores : 10 apicid : 48 initial apicid : 48 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 16 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 9 cpu cores : 10 apicid : 50 initial apicid : 50 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 17 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 10 cpu cores : 10 apicid : 52 initial apicid : 52 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 18 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 11 cpu cores : 10 apicid : 54 initial apicid : 54 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 19 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 2801.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 12 cpu cores : 10 apicid : 56 initial apicid : 56 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 20 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 0 cpu cores : 10 apicid : 1 initial apicid : 1 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 21 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 1 cpu cores : 10 apicid : 3 initial apicid : 3 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 22 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 2 cpu cores : 10 apicid : 5 initial apicid : 5 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 23 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 3 cpu cores : 10 apicid : 7 initial apicid : 7 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 24 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 4 cpu cores : 10 apicid : 9 initial apicid : 9 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 25 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 8 cpu cores : 10 apicid : 17 initial apicid : 17 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 26 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 9 cpu cores : 10 apicid : 19 initial apicid : 19 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 27 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 10 cpu cores : 10 apicid : 21 initial apicid : 21 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 28 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 11 cpu cores : 10 apicid : 23 initial apicid : 23 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 29 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 0 siblings : 20 core id : 12 cpu cores : 10 apicid : 25 initial apicid : 25 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5586.71 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 30 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 0 cpu cores : 10 apicid : 33 initial apicid : 33 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 31 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 1 cpu cores : 10 apicid : 35 initial apicid : 35 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 32 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 2 cpu cores : 10 apicid : 37 initial apicid : 37 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 33 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 3 cpu cores : 10 apicid : 39 initial apicid : 39 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 34 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 4 cpu cores : 10 apicid : 41 initial apicid : 41 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 35 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 8 cpu cores : 10 apicid : 49 initial apicid : 49 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 36 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 9 cpu cores : 10 apicid : 51 initial apicid : 51 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 37 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 10 cpu cores : 10 apicid : 53 initial apicid : 53 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 38 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 11 cpu cores : 10 apicid : 55 initial apicid : 55 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: processor : 39 vendor_id : GenuineIntel cpu family : 6 model : 62 model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz stepping : 4 cpu MHz : 1200.000 cache size : 25600 KB physical id : 1 siblings : 20 core id : 12 cpu cores : 10 apicid : 57 initial apicid : 57 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 5585.83 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management: go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_2logical.txt000066400000000000000000000027141510742411500324470ustar00rootroot00000000000000processor : 0 vendor_id : AuthenticAMD cpu family : 15 model : 33 model name : Dual Core AMD Opteron(tm) Processor 270 stepping : 2 cpu MHz : 2004.546 cache size : 1024 KB physical id : 0 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt lm 3dnowext 3dnow pni lahf_lm cmp_lega cy bogomips : 4011.21 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp processor : 1 vendor_id : AuthenticAMD cpu family : 15 model : 33 model name : Dual Core AMD Opteron(tm) Processor 270 stepping : 2 cpu MHz : 2004.546 cache size : 1024 KB physical id : 1 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt lm 3dnowext 3dnow up pni lahf_lm cmp_l egacy bogomips : 4011.21 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_4logical.txt000066400000000000000000000011101510742411500324360ustar00rootroot00000000000000processor : 0 model name : Intel(R) Xeon(TM) CPU 3.60GHz cache size : 1024 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 1 processor : 1 model name : Intel(R) Xeon(TM) CPU 3.60GHz cache size : 1024 KB physical id : 3 siblings : 2 core id : 0 cpu cores : 1 processor : 2 model name : Intel(R) Xeon(TM) CPU 3.60GHz cache size : 1024 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 1 processor : 3 model name : Intel(R) Xeon(TM) CPU 3.60GHz cache size : 1024 KB physical id : 3 siblings : 2 core id : 0 cpu cores : 1 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_4core_4logical.txt000066400000000000000000000012161510742411500324470ustar00rootroot00000000000000processor : 0 model name : Intel(R) Xeon(R) CPU 5160 @ 3.00GHz cache size : 4096 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 2 processor : 1 model name : Intel(R) Xeon(R) CPU 5160 @ 3.00GHz cache size : 4096 KB physical id : 0 siblings : 2 core id : 1 cpu cores : 2 processor : 2 model name : Intel(R) Xeon(R) CPU 5160 @ 3.00GHz cache size : 4096 KB physical id : 3 siblings : 2 core id : 0 cpu cores : 2 processor : 3 model name : Intel(R) Xeon(R) CPU 5160 @ 3.00GHz cache size : 4096 KB physical id : 3 siblings : 2 core id : 1 cpu cores : 2 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/4pack_4core_4logical.txt000066400000000000000000000061241510742411500324540ustar00rootroot00000000000000processor : 0 vendor_id : AuthenticAMD cpu family : 15 model : 65 model name : Dual-Core AMD Opteron(tm) Processor 2218 HE stepping : 3 cpu MHz : 2599.998 cache size : 1024 KB physical id : 0 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow pni cx16 lahf _lm cmp_legacy svm extapic cr8_legacy bogomips : 5202.15 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp tm stc processor : 1 vendor_id : AuthenticAMD cpu family : 15 model : 65 model name : Dual-Core AMD Opteron(tm) Processor 2218 HE stepping : 3 cpu MHz : 2599.998 cache size : 1024 KB physical id : 1 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow up pni cx16 l ahf_lm cmp_legacy svm extapic cr8_legacy bogomips : 5202.15 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp tm stc processor : 2 vendor_id : AuthenticAMD cpu family : 15 model : 65 model name : Dual-Core AMD Opteron(tm) Processor 2218 HE stepping : 3 cpu MHz : 2599.998 cache size : 1024 KB physical id : 2 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow up pni cx16 l ahf_lm cmp_legacy svm extapic cr8_legacy bogomips : 5202.15 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp tm stc processor : 3 vendor_id : AuthenticAMD cpu family : 15 model : 65 model name : Dual-Core AMD Opteron(tm) Processor 2218 HE stepping : 3 cpu MHz : 2599.998 cache size : 1024 KB physical id : 3 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 1 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush mmx fx sr sse sse2 ht syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow up pni cx16 l ahf_lm cmp_legacy svm extapic cr8_legacy bogomips : 5202.15 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 40 bits physical, 48 bits virtual power management: ts fid vid ttp tm stc go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/8pack_8core_8logical.txt000066400000000000000000000134231510742411500324700ustar00rootroot00000000000000processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 0 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc pni monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 1 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 2 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 2 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 3 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 3 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 4 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 4 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 5 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 5 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 6 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 6 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: processor : 7 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB physical id : 7 siblings : 1 core id : 0 cpu cores : 1 fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm syscall nx lm constant_tsc up pni monitor ds_cpl v mx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 4654.10 clflush size : 64 cache_alignment : 64 address sizes : 38 bits physical, 48 bits virtual power management: go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/README.md000066400000000000000000000025561510742411500273230ustar00rootroot00000000000000These tests are for determining the numbers of physical packages, physical cores, and logical processors from the data returned by /proc/cpuinfo on Linux hosts. Each text file in this directory is the output of /proc/cpuinfo on various machines. The names of all test files should be of the form `Apack_Bcore_Clogical.txt` where `A`, `B`, and `C` are integers or the character `X`. For example, a single quad-core processor without hyperthreading would correspond to `1pack_4core_4logical.txt`, while two 6-core processors with hyperthreading would correspond to `2pack_12core_24logical.txt`, and would be pretty sweet. Using `A`, `B`, and `C` from above, code processing the text in these files should produce the following expected values: | property | value | | -------------------- |---------| | # physical packages | `A` | | # physical cores | `B` | | # logical processors | `C` | (Obviously, the processing code should do this with no knowledge of the filenames.) If any of `A`, `B`, or `C` are the character `X` instead of an integer, then processing code should not return a value (return `null`, return `nil`, raise an exception... whatever makes most sense for your agent). There is a malformed.txt file which is a random file that does not adhere to any /proc/cpuinfo format. The expected result is `null` for packages, cores and processors. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/Xpack_Xcore_2logical.txt000066400000000000000000000024131510742411500325570ustar00rootroot00000000000000processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm pbe nx lm constant_tsc pni monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 5821.98 clflush size : 64 processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Xeon(R) CPU E5345 @ 2.33GHz stepping : 11 cpu MHz : 2327.498 cache size : 4096 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu tsc msr pae mce cx8 apic mca cmov pat pse36 clflush dts ac pi mmx fxsr sse sse2 ss ht tm pbe nx lm constant_tsc up pni monitor ds_cpl vmx e st tm2 ssse3 cx16 xtpr dca lahf_lm bogomips : 5821.98 clflush size : 64 go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_cpuinfo/malformed_file.txt000066400000000000000000000001301510742411500315340ustar00rootroot00000000000000This is a random text file that does NOT adhere to the /proc/cpuinfo format. xxxYYYZZz go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_meminfo/000077500000000000000000000000001510742411500260235ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_meminfo/README.md000066400000000000000000000006441510742411500273060ustar00rootroot00000000000000These tests are for determining the physical memory from the data returned by /proc/meminfo on Linux hosts. The total physical memory of the linux system is reported as part of the environment values. The key used by the Python agent is 'Total Physical Memory (MB)'. The names of all test files should be of the form `meminfo_nnnnMB.txt`. The value `nnnn` in the filename is the physical memory of that system in MB. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/proc_meminfo/meminfo_4096MB.txt000066400000000000000000000023751510742411500311260ustar00rootroot00000000000000MemTotal: 4194304 kB MemFree: 931724 kB Buffers: 146992 kB Cached: 545044 kB SwapCached: 0 kB Active: 551644 kB Inactive: 454660 kB Active(anon): 315628 kB Inactive(anon): 9084 kB Active(file): 236016 kB Inactive(file): 445576 kB Unevictable: 0 kB Mlocked: 0 kB HighTotal: 1183624 kB HighFree: 295288 kB LowTotal: 877428 kB LowFree: 636436 kB SwapTotal: 1046524 kB SwapFree: 1046524 kB Dirty: 72 kB Writeback: 0 kB AnonPages: 314416 kB Mapped: 127944 kB Shmem: 10448 kB Slab: 75852 kB SReclaimable: 59144 kB SUnreclaim: 16708 kB KernelStack: 2984 kB PageTables: 7552 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 2077048 kB Committed_AS: 2433452 kB VmallocTotal: 122880 kB VmallocUsed: 23288 kB VmallocChunk: 98348 kB HardwareCorrupted: 0 kB AnonHugePages: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 12280 kB DirectMap2M: 901120 kB go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rules.json000066400000000000000000000212451510742411500253770ustar00rootroot00000000000000[ { "testname":"replace first", "rules":[{"match_expression":"(psi)", "replacement":"gamma", "ignore":false, "eval_order":0}, {"match_expression":"^/userid/.*/folderid", "replacement":"/userid/*/folderid/*", "ignore":false, "eval_order":1}, {"match_expression":"/need_not_be_first_segment/.*", "replacement":"*/need_not_be_first_segment/*", "ignore":false, "eval_order":2}], "tests": [ {"input":"/alpha/psi/beta", "expected":"/alpha/gamma/beta"}, {"input":"/psi/beta", "expected":"/gamma/beta"}, {"input":"/alpha/psi", "expected":"/alpha/gamma"}, {"input":"/userid/123abc/folderid/qwerty8855", "expected":"/userid/*/folderid/*/qwerty8855"}, {"input":"/first/need_not_be_first_segment/uiop", "expected":"/first*/need_not_be_first_segment/*"} ] }, { "testname":"resource normalization rule", "rules":[{"match_expression":"(.*)/[^/]*.(bmp|css|gif|ico|jpg|jpeg|js|png)$", "replacement":"\\1/*.\\2", "ignore":false, "eval_order":1}], "tests": [ {"input":"/test/dude/flower.jpg", "expected":"/test/dude/*.jpg"}, {"input":"/DUDE.ICO", "expected":"/*.ICO"} ] }, { "testname":"ignore rule", "rules":[{"match_expression":"^/artists/az/(.*)/(.*)$", "replacement":"/artists/az/*/\\2", "ignore":true, "eval_order":11}], "tests": [ {"input":"/artists/az/veritas/truth.jhtml", "expected":null} ] }, { "testname":"hexadecimal each segment rule", "rules":[{"match_expression":"^[0-9a-f]*[0-9][0-9a-f]*$", "replacement":"*", "ignore":false, "eval_order":1, "each_segment":true}], "tests": [ {"input":"/test/1axxx/4babe/cafe222/bad/a1b2c3d3e4f5/ABC123/x999/111", "expected":"/test/1axxx/*/*/bad/*/*/x999/*"}, {"input":"/test/4/dude", "expected":"/test/*/dude"}, {"input":"/test/babe4/999x", "expected":"/test/*/999x"}, {"input":"/glass/resource/vase/images/9ae1283", "expected":"/glass/resource/vase/images/*"}, {"input":"/test/4/dude.jsp", "expected":"/test/*/dude.jsp"}, {"input":"/glass/resource/vase/images/add", "expected":"/glass/resource/vase/images/add"} ] }, { "testname":"url encoded segments rule", "rules":[{"match_expression":"(.*)%(.*)", "replacement":"*", "ignore":false, "eval_order":1, "each_segment":true, "terminate_chain":false, "replace_all":false}], "tests": [ {"input":"/test/%%%/bad%%/a1b2%c3%d3e4f5/x999/111%", "expected":"/test/*/*/*/x999/*"}, {"input":"/add-resource/vmqoiearks%1B%3R", "expected":"/add-resource/*"} ] }, { "testname":"remove all ticks", "rules":[{"match_expression":"([^']*)'+", "replacement":"\\1", "ignore":false, "eval_order":1, "each_segment":false, "replace_all":true}], "tests": [ {"input":"/test/'''/b'a''d''/a1b2'c3'd3e4f5/x999/111'", "expected":"/test//bad/a1b2c3d3e4f5/x999/111"} ] }, { "testname":"number rule", "rules":[{"match_expression":"\\d+", "replacement":"*", "ignore":false, "eval_order":1, "each_segment":false, "replace_all":true}], "tests": [ {"input":"/solr/shard03/select", "expected":"/solr/shard*/select"}, {"input":"/hey/r2d2", "expected":"/hey/r*d*"} ] }, { "testname":"custom rules", "rules": [ {"match_expression":"^/([^/]*=[^/]*&?)+", "replacement":"/all_params", "ignore":false, "eval_order":0, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/.*/PARAMS/(article|legacy_article|post|product)/.*", "replacement":"/*/PARAMS/\\1/*", "ignore":false, "eval_order":14, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/test/(.*)", "replacement":"/dude", "ignore":false, "eval_order":1, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/blah/(.*)", "replacement":"/\\1", "ignore":false, "eval_order":2, "each_segment":false, "terminate_chain":true}, {"match_expression":"/.*(dude|man)", "replacement":"/*.\\1", "ignore":false, "eval_order":3, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/(bob)", "replacement":"/\\1ert/\\1/\\1ertson", "ignore":false, "eval_order":4, "each_segment":false, "terminate_chain":true}, {"match_expression":"/foo(.*)", "ignore":true, "eval_order":5, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/(keep)(/)(me)", "replacement":"/\\1\\2\\3", "ignore":false, "eval_order":6, "each_segment":false, "terminate_chain":true}, {"match_expression":"^/(keep)(/)(me)", "replacement":"/you_werent_kept", "ignore":false, "eval_order":7, "each_segment":false, "terminate_chain":true} ], "tests": [ {"input":"/xs=zs&fly=*&row=swim&id=*&", "expected":"/all_params"}, {"input":"/zip-zap/PARAMS/article/*", "expected":"/*/PARAMS/article/*"}, {"input":"/bob", "expected":"/bobert/bob/bobertson"}, {"input":"/test/foobar", "expected":"/dude"}, {"input":"/bar/test", "expected":"/bar/test"}, {"input":"/blah/test/man", "expected":"/test/man"}, {"input":"/oh/hey.dude", "expected":"/*.dude"}, {"input":"/oh/hey/what/up.man", "expected":"/*.man"}, {"input":"/foo", "expected":null}, {"input":"/foo/foobar", "expected":null}, {"input":"/keep/me", "expected":"/keep/me"} ] }, { "testname":"chained rules", "rules": [ {"match_expression":"^[0-9a-f]*[0-9][0-9a-f]*$", "replacement":"*", "ignore":false, "eval_order":1, "each_segment":true, "terminate_chain":false}, {"match_expression":"(.*)/fritz/(.*)", "replacement":"\\1/karl/\\2", "ignore":false, "eval_order":11, "each_segment":false, "terminate_chain":true} ], "tests": [ {"input":"/test/1axxx/4babe/fritz/x999/111", "expected":"/test/1axxx/*/karl/x999/*"} ] }, { "testname":"rule ordering (two rules match, but only one is applied due to ordering)", "rules": [ {"match_expression":"/test/(.*)", "replacement":"/el_duderino", "ignore":false, "eval_order":37}, {"match_expression":"/test/(.*)", "replacement":"/dude", "ignore":false, "eval_order":1}, {"match_expression":"/blah/(.*)", "replacement":"/$1", "ignore":false, "eval_order":2}, {"match_expression":"/foo(.*)", "ignore":true, "eval_order":3} ], "tests": [ {"input":"/test/foobar", "expected":"/dude"} ] }, { "testname":"stable rule sorting", "rules": [ {"match_expression":"/test/(.*)", "replacement":"/you_first", "ignore":false, "eval_order":0}, {"match_expression":"/test/(.*)", "replacement":"/no_you", "ignore":false, "eval_order":0}, {"match_expression":"/test/(.*)", "replacement":"/please_after_you", "ignore":false, "eval_order":0} ], "tests": [ {"input":"/test/polite_seattle_drivers", "expected":"/you_first"} ] }, { "testname":"custom rule chaining", "rules": [ {"match_expression":"(.*)/robertson(.*)", "replacement":"\\1/LAST_NAME\\2", "ignore":false, "eval_order":0, "terminate_chain":false}, {"match_expression":"^/robert(.*)", "replacement":"/bob\\1", "ignore":false, "eval_order":1, "terminate_chain":true}, {"match_expression":"/LAST_NAME", "replacement":"/fail", "ignore":false, "eval_order":2, "terminate_chain":true} ], "tests": [ {"input":"/robert/robertson", "expected":"/bob/LAST_NAME"} ] }, { "testname":"saxon's test", "rules":[{"match_expression":"^(?!account|application).*", "replacement":"*", "ignore":false, "eval_order":0, "each_segment":true}], "tests": [ {"input":"/account/myacc/application/test", "expected":"/account/*/application/*"}, {"input":"/oh/dude/account/myacc/application", "expected":"/*/*/account/*/application"} ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_client_config.json000066400000000000000000000052311510742411500277300ustar00rootroot00000000000000[ { "testname":"all fields present", "apptime_milliseconds":5, "queuetime_milliseconds":3, "browser_monitoring.attributes.enabled":true, "transaction_name":"WebTransaction/brink/of/glory", "license_key":"0000111122223333444455556666777788889999", "connect_reply": { "beacon":"my_beacon", "browser_key":"my_browser_key", "application_id":"my_application_id", "error_beacon":"my_error_beacon", "js_agent_file":"my_js_agent_file" }, "user_attributes":{"alpha":"beta"}, "expected": { "beacon":"my_beacon", "licenseKey":"my_browser_key", "applicationID":"my_application_id", "transactionName":"Z1VSZENQX0JTUUZbXF4fUkJYX1oeXVQdVV9fQkk=", "queueTime":3, "applicationTime":5, "atts":"SxJFEgtKE1BeQlpTEQoSUlVFUBNMTw==", "errorBeacon":"my_error_beacon", "agent":"my_js_agent_file" } }, { "testname":"browser_monitoring.attributes.enabled disabled", "apptime_milliseconds":5, "queuetime_milliseconds":3, "browser_monitoring.attributes.enabled":false, "transaction_name":"WebTransaction/brink/of/glory", "license_key":"0000111122223333444455556666777788889999", "connect_reply": { "beacon":"my_beacon", "browser_key":"my_browser_key", "application_id":"my_application_id", "error_beacon":"my_error_beacon", "js_agent_file":"my_js_agent_file" }, "user_attributes":{"alpha":"beta"}, "expected": { "beacon":"my_beacon", "licenseKey":"my_browser_key", "applicationID":"my_application_id", "transactionName":"Z1VSZENQX0JTUUZbXF4fUkJYX1oeXVQdVV9fQkk=", "queueTime":3, "applicationTime":5, "atts":"", "errorBeacon":"my_error_beacon", "agent":"my_js_agent_file" } }, { "testname":"empty js_agent_file", "apptime_milliseconds":5, "queuetime_milliseconds":3, "browser_monitoring.attributes.enabled":true, "transaction_name":"WebTransaction/brink/of/glory", "license_key":"0000111122223333444455556666777788889999", "connect_reply": { "beacon":"my_beacon", "browser_key":"my_browser_key", "application_id":"my_application_id", "error_beacon":"my_error_beacon", "js_agent_file":"" }, "user_attributes":{"alpha":"beta"}, "expected": { "beacon":"my_beacon", "licenseKey":"my_browser_key", "applicationID":"my_application_id", "transactionName":"Z1VSZENQX0JTUUZbXF4fUkJYX1oeXVQdVV9fQkk=", "queueTime":3, "applicationTime":5, "atts":"SxJFEgtKE1BeQlpTEQoSUlVFUBNMTw==", "errorBeacon":"my_error_beacon", "agent":"" } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/000077500000000000000000000000001510742411500315115ustar00rootroot00000000000000close-body-in-comment.html000066400000000000000000000003571510742411500364310ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_footer_insertion_location Comment contains a close body tag

The quick brown fox jumps over the lazy dog.

EXPECTED_RUM_FOOTER_LOCATION dynamic-iframe.html000066400000000000000000000010641510742411500352060ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_footer_insertion_location Dynamic iframe Generation

The quick brown fox jumps over the lazy dog.

EXPECTED_RUM_FOOTER_LOCATION go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/000077500000000000000000000000001510742411500314615ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/basic.html000066400000000000000000000002761510742411500334350ustar00rootroot00000000000000 EXPECTED_RUM_LOADER_LOCATION im a title im some body text body_with_attributes.html000066400000000000000000000001651510742411500365300ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_locationEXPECTED_RUM_LOADER_LOCATION This isn't great HTML but it's what we've got. charset_tag.html000066400000000000000000000003311510742411500345510ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text charset_tag_after_x_ua_tag.html000066400000000000000000000004021510742411500376000ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text charset_tag_before_x_ua_tag.html000066400000000000000000000004021510742411500377410ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text charset_tag_with_spaces.html000066400000000000000000000003361510742411500371470ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text comments1.html000066400000000000000000000013441510742411500342000ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location OPT® Cribbed from the Java agent comments2.html000066400000000000000000000013361510742411500342020ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location OPT® Cribbed from the Java agent content_type_charset_tag.html000066400000000000000000000004061510742411500373470ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text content_type_charset_tag_after_x_ua_tag.html000066400000000000000000000004571510742411500424050ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text content_type_charset_tag_before_x_ua_tag.html000066400000000000000000000004571510742411500425460ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text gt_in_quotes1.html000066400000000000000000000013631510742411500350540ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location OPT® Cribbed from the Java agent gt_in_quotes2.html000066400000000000000000000013361510742411500350550ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location OPT® Cribbed from the Java agent gt_in_quotes_mismatch.html000066400000000000000000000013431510742411500366560ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location ication" content="xxxx" /> OPT® Cribbed from the Java agent gt_in_single_quotes1.html000066400000000000000000000013661510742411500364200ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location OPT® Cribbed from the Java agent gt_in_single_quotes_mismatch.html000066400000000000000000000013661510742411500402240ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location .01' content='yyyy\' /> OPT® Cribbed from the Java agent head_with_attributes.html000066400000000000000000000003301510742411500364660ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location EXPECTED_RUM_LOADER_LOCATION im a title im some body text incomplete_non_meta_tags.html000066400000000000000000000002271510742411500373260ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location EXPECTED_RUM_LOADER_LOCATION EXPECTED_RUM_LOADER_LOCATION Cribbed from the Java agent no_header.html000066400000000000000000000004151510742411500342140ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location EXPECTED_RUM_LOADER_LOCATION Cribbed from the Java agent no_html_and_no_header.html000066400000000000000000000001341510742411500365540ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_locationEXPECTED_RUM_LOADER_LOCATION This isn't great HTML but it's what we've got. no_start_header.html000066400000000000000000000004301510742411500354260ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location EXPECTED_RUM_LOADER_LOCATION Cribbed from the Java agent go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script1.html000066400000000000000000000013231510742411500337330ustar00rootroot00000000000000 EXPECTED_RUM_LOADER_LOCATION Castor go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script2.html000066400000000000000000000013031510742411500337320ustar00rootroot00000000000000 EXPECTED_RUM_LOADER_LOCATION Castor x_ua_meta_tag.html000066400000000000000000000003471510742411500350710ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text x_ua_meta_tag_multiline.html000066400000000000000000000003631510742411500371510ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text x_ua_meta_tag_multiple_tags.html000066400000000000000000000005611510742411500400200ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text x_ua_meta_tag_spaces_around_equals.html000066400000000000000000000003521510742411500413450ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text x_ua_meta_tag_with_others.html000066400000000000000000000004431510742411500375050ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text x_ua_meta_tag_with_spaces.html000066400000000000000000000003551510742411500374610ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/rum_loader_insertion_location im a title EXPECTED_RUM_LOADER_LOCATION im some body text go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/sql_obfuscation/000077500000000000000000000000001510742411500265415ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/sql_obfuscation/README.md000066400000000000000000000044331510742411500300240ustar00rootroot00000000000000These test cases cover obfuscation (more properly, masking) of literal values from SQL statements captured by agents. SQL statements may be captured and attached to transaction trace nodes, or to slow SQL traces. `sql_obfuscation.json` contains an array of test cases. The inputs for each test case are in the `sql` property of each object. Each test case also has an `obfuscated` property which is an array containing at least one valid output. Test cases also have a `dialects` property, which is an array of strings which specify which sql dialects the test should apply to. See "SQL Syntax Documentation" list below. This is relevant because for example, PostgreSQL uses different identifier and string quoting rules than MySQL (most notably, double-quoted string literals are not allowed in PostgreSQL, where double-quotes are instead used around identifiers). Test cases may also contain the following properties: * `malformed`: (boolean) test SQL queries which are not valid SQL in any quoting mode. Some agents may choose to attempt to obfuscate these cases, and others may instead just replace the query entirely with a placeholder message. In some agents (such as .Net), invalid SQL is caught by the driver - which throws an exception - before the obfuscation method is called. In those cases, implementation of the obfuscation test may be unnecessary. * `pathological`: (boolean) tests which are designed specifically to break specific methods of obfuscation, or contain patterns that are known to be difficult to handle correctly * `comments`: an array of strings that could be usefult for understanding the test. The following database documentation may be helpful in understanding these test cases: * [MySQL String Literals](http://dev.mysql.com/doc/refman/5.5/en/string-literals.html) * [PostgreSQL String Constants](http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS) SQL Syntax Documentation: * [MySQL](http://dev.mysql.com/doc/refman/5.5/en/language-structure.html) * [PostgreSQL](http://www.postgresql.org/docs/8.4/static/sql-syntax.html) * [Cassandra](http://docs.datastax.com/en/cql/3.1/cql/cql_reference/cql_lexicon_c.html) * [Oracle](http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/langelems.htm) * [SQLite](https://www.sqlite.org/lang.html) go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/sql_obfuscation/sql_obfuscation.json000066400000000000000000000376431510742411500326440ustar00rootroot00000000000000[ { "name": "back_quoted_identifiers.mysql", "obfuscated": [ "SELECT `t001`.`c2` FROM `t001` WHERE `t001`.`c2` = ? AND c3=? LIMIT ?" ], "dialects": [ "mysql" ], "sql": "SELECT `t001`.`c2` FROM `t001` WHERE `t001`.`c2` = 'value' AND c3=\"othervalue\" LIMIT ?" }, { "name": "comment_delimiters_in_double_quoted_strings", "obfuscated": [ "SELECT * FROM t WHERE foo=? AND baz=?" ], "dialects": [ "mssql", "mysql" ], "sql": "SELECT * FROM t WHERE foo=\"bar/*\" AND baz=\"whatever */qux\"" }, { "name": "comment_delimiters_in_single_quoted_strings", "obfuscated": [ "SELECT * FROM t WHERE foo=? AND baz=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM t WHERE foo='bar/*' AND baz='whatever */qux'" }, { "name": "double_quoted_identifiers.postgres", "obfuscated": [ "SELECT \"t001\".\"c2\" FROM \"t001\" WHERE \"t001\".\"c2\" = ? AND c3=? LIMIT ?" ], "dialects": [ "postgres", "oracle" ], "sql": "SELECT \"t001\".\"c2\" FROM \"t001\" WHERE \"t001\".\"c2\" = 'value' AND c3=1234 LIMIT 1" }, { "name": "end_of_line_comment_in_double_quoted_string", "obfuscated": [ "SELECT * FROM t WHERE foo=? AND\n baz=?" ], "dialects": [ "mssql", "mysql" ], "sql": "SELECT * FROM t WHERE foo=\"bar--\" AND\n baz=\"qux--\"" }, { "name": "end_of_line_comment_in_single_quoted_string", "obfuscated": [ "SELECT * FROM t WHERE foo=? AND\n baz=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM t WHERE foo='bar--' AND\n baz='qux--'" }, { "name": "end_of_query_comment_cstyle", "obfuscated": [ "SELECT * FROM foo WHERE bar=? ?", "SELECT * FROM foo WHERE bar=? " ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM foo WHERE bar='baz' /* Hide Me */" }, { "name": "end_of_query_comment_doubledash", "obfuscated": [ "SELECT * FROM foobar WHERE password=?\n?", "SELECT * FROM foobar WHERE password=?\n" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM foobar WHERE password='hunter2'\n-- No peeking!" }, { "name": "end_of_query_comment_hash", "obfuscated": [ "SELECT foo, bar FROM baz WHERE password=? ?", "SELECT foo, bar FROM baz WHERE password=? " ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT foo, bar FROM baz WHERE password='hunter2' # Secret" }, { "name": "escape_string_constants.postgres", "sql": "SELECT \"col1\", \"col2\" from \"table\" WHERE \"col3\"=E'foo\\'bar\\\\baz' AND country=e'foo\\'bar\\\\baz'", "obfuscated": [ "SELECT \"col1\", \"col2\" from \"table\" WHERE \"col3\"=E?", "SELECT \"col1\", \"col2\" from \"table\" WHERE \"col3\"=E? AND country=e?" ], "dialects": [ "postgres" ], "comments": [ "PostgreSQL supports an alternate string quoting mode where backslash escape", "sequences are interpreted.", "See: http://www.postgresql.org/docs/9.3/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE" ] }, { "name": "multiple_literal_types.mysql", "obfuscated": [ "INSERT INTO `X` values(?,?, ? , ?, ?)" ], "dialects": [ "mysql" ], "sql": "INSERT INTO `X` values(\"test\",0, 1 , 2, 'test')" }, { "name": "numbers_in_identifiers", "obfuscated": [ "SELECT c11.col1, c22.col2 FROM table c11, table c22 WHERE value=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT c11.col1, c22.col2 FROM table c11, table c22 WHERE value='nothing'" }, { "name": "numeric_literals", "sql": "INSERT INTO X VALUES(1, 23456, 123.456, 99+100)", "obfuscated": [ "INSERT INTO X VALUES(?, ?, ?, ?+?)", "INSERT INTO X VALUES(?, ?, ?.?, ?+?)" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ] }, { "name": "string_double_quoted.mysql", "obfuscated": [ "SELECT * FROM table WHERE name=? AND value=?" ], "dialects": [ "mysql" ], "sql": "SELECT * FROM table WHERE name=\"foo\" AND value=\"don't\"" }, { "name": "string_single_quoted", "obfuscated": [ "SELECT * FROM table WHERE name=? AND value = ?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM table WHERE name='foo' AND value = 'bar'" }, { "name": "string_with_backslash_and_twin_single_quotes", "obfuscated": [ "SELECT * FROM table WHERE col=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM table WHERE col='foo\\''bar'", "comments": [ "If backslashes are being ignored in single-quoted strings", "(standard_conforming_strings=on in PostgreSQL, or NO_BACKSLASH_ESCAPES is on", "in MySQL), then this is valid SQL." ] }, { "name": "string_with_embedded_double_quote", "obfuscated": [ "SELECT * FROM table WHERE col1=? AND col2=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM table WHERE col1='foo\"bar' AND col2='what\"ever'" }, { "name": "string_with_embedded_newline", "obfuscated": [ "select * from accounts where accounts.name != ? order by accounts.name" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "select * from accounts where accounts.name != 'dude \n newline' order by accounts.name" }, { "name": "string_with_embedded_single_quote.mysql", "obfuscated": [ "SELECT * FROM table WHERE col1=? AND col2=?" ], "dialects": [ "mysql" ], "sql": "SELECT * FROM table WHERE col1=\"don't\" AND col2=\"won't\"" }, { "name": "string_with_escaped_quotes.mysql", "sql": "INSERT INTO X values('', 'jim''s ssn',0, 1 , 'jim''s son''s son', \"\"\"jim''s\"\" hat\", \"\\\"jim''s secret\\\"\")", "obfuscated": [ "INSERT INTO X values(?, ?,?, ? , ?, ?, ?", "INSERT INTO X values(?, ?,?, ? , ?, ?, ?)" ], "dialects": [ "mysql" ] }, { "name": "string_with_trailing_backslash", "sql": "SELECT * FROM table WHERE name='foo\\' AND color='blue'", "obfuscated": [ "SELECT * FROM table WHERE name=?", "SELECT * FROM table WHERE name=? AND color=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "comments": [ "If backslashes are being ignored in single-quoted strings", "(standard_conforming_strings=on in PostgreSQL, or NO_BACKSLASH_ESCAPES is on", "in MySQL), then this is valid SQL." ] }, { "name": "string_with_trailing_escaped_backslash.mysql", "obfuscated": [ "SELECT * FROM table WHERE foo=?" ], "dialects": [ "mysql" ], "sql": "SELECT * FROM table WHERE foo=\"this string ends with a backslash\\\\\"" }, { "name": "string_with_trailing_escaped_backslash_single_quoted", "obfuscated": [ "SELECT * FROM table WHERE foo=?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "SELECT * FROM table WHERE foo='this string ends with a backslash\\\\'" }, { "name": "string_with_trailing_escaped_quote", "sql": "SELECT * FROM table WHERE name='foo\\'' AND color='blue'", "obfuscated": [ "SELECT * FROM table WHERE name=?", "SELECT * FROM table WHERE name=? AND color=?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ] }, { "name": "string_with_twin_single_quotes", "obfuscated": [ "INSERT INTO X values(?, ?,?, ? , ?)" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "sql": "INSERT INTO X values('', 'a''b c',0, 1 , 'd''e f''s h')" }, { "name": "pathological/end_of_line_comments_with_quotes", "sql": "SELECT * FROM t WHERE -- '\n bar='baz' -- '", "obfuscated": [ "SELECT * FROM t WHERE ?\n bar=? ?", "SELECT * FROM t WHERE ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "pathological/mixed_comments_and_quotes", "sql": "SELECT * FROM t WHERE /* ' */ \n bar='baz' -- '", "obfuscated": [ "SELECT * FROM t WHERE ? \n bar=? ?", "SELECT * FROM t WHERE ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "pathological/mixed_quotes_comments_and_newlines", "sql": "SELECT * FROM t WHERE -- '\n /* ' */ c2='xxx' /* ' */\n c='x\n xx' -- '", "obfuscated": [ "SELECT * FROM t WHERE ?\n ? c2=? ?\n c=? ?", "SELECT * FROM t WHERE ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "pathological/mixed_quotes_end_of_line_comments", "sql": "SELECT * FROM t WHERE -- '\n c='x\n xx' -- '", "obfuscated": [ "SELECT * FROM t WHERE ?\n c=? ?", "SELECT * FROM t WHERE ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "pathological/quote_delimiters_in_comments", "sql": "SELECT * FROM foo WHERE col='value1' AND /* don't */ col2='value1' /* won't */", "obfuscated": [ "SELECT * FROM foo WHERE col=? AND ? col2=? ?", "SELECT * FROM foo WHERE col=? AND ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "malformed/unterminated_double_quoted_string.mysql", "sql": "SELECT * FROM table WHERE foo='bar' AND baz=\"nothing to see here'", "dialects": [ "mysql" ], "obfuscated": [ "?" ], "malformed": true }, { "name": "malformed/unterminated_single_quoted_string", "sql": "SELECT * FROM table WHERE foo='bar' AND baz='nothing to see here", "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "obfuscated": [ "?" ], "malformed": true }, { "name": "dollar_quotes", "sql": "SELECT * FROM \"foo\" WHERE \"foo\" = $a$dollar quotes can be $b$nested$b$$a$ and bar = 'baz'", "obfuscated": [ "SELECT * FROM \"foo\" WHERE \"foo\" = ? and bar = ?" ], "dialects": [ "postgres" ] }, { "name": "variable_substitution_not_mistaken_for_dollar_quotes", "sql": "INSERT INTO \"foo\" (\"bar\", \"baz\", \"qux\") VALUES ($1, $2, $3) RETURNING \"id\"", "obfuscated": [ "INSERT INTO \"foo\" (\"bar\", \"baz\", \"qux\") VALUES ($?, $?, $?) RETURNING \"id\"" ], "dialects": [ "postgres" ] }, { "name": "non_quote_escape", "sql": "select * from foo where bar = 'some\\tthing' and baz = 10", "obfuscated": [ "select * from foo where bar = ? and baz = ?" ], "dialects": [ "mssql", "mysql", "postgres", "oracle", "cassandra", "sqlite" ] }, { "name": "end_of_string_backslash_and_line_comment_with_quite", "sql": "select * from users where user = 'user1\\' password = 'hunter 2' -- ->don't count this quote", "obfuscated": [ "select * from users where user = ?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ], "pathological": true }, { "name": "oracle_bracket_quote", "sql": "select * from foo where bar=q'[baz's]' and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "oracle" ] }, { "name": "oracle_brace_quote", "sql": "select * from foo where bar=q'{baz's}' and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "oracle" ] }, { "name": "oracle_angle_quote", "sql": "select * from foo where bar=q'' and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "oracle" ] }, { "name": "oracle_paren_quote", "sql": "select * from foo where bar=q'(baz's)' and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "oracle" ] }, { "name": "cassandra_blobs", "sql": "select * from foo where bar=0xabcdef123 and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "cassandra", "sqlite" ] }, { "name": "hex_literals", "sql": "select * from foo where bar=0x2F and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "mysql", "cassandra", "sqlite" ] }, { "name": "exponential_literals", "sql": "select * from foo where bar=1.234e-5 and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ] }, { "name": "negative_integer_literals", "sql": "select * from foo where bar=-1.234e-5 and x=-5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "sqlite" ] }, { "name": "uuid", "sql": "select * from foo where bar=01234567-89ab-cdef-0123-456789abcdef and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "postgres", "cassandra" ] }, { "name": "uuid_with_braces", "sql": "select * from foo where bar={01234567-89ab-cdef-0123-456789abcdef} and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "postgres" ] }, { "name": "uuid_no_dashes", "sql": "select * from foo where bar=0123456789abcdef0123456789abcdef and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "postgres" ] }, { "name": "uuid_random_dashes", "sql": "select * from foo where bar={012-345678-9abc-def012345678-9abcdef} and x=5", "obfuscated": [ "select * from foo where bar=? and x=?" ], "dialects": [ "postgres" ] }, { "name": "booleans", "sql": "select * from truestory where bar=true and x=FALSE", "obfuscated": [ "select * from truestory where bar=? and x=?" ], "dialects": [ "mysql", "postgres", "cassandra", "sqlite" ] }, { "name": "in_clause_digits", "sql": "select * from foo where bar IN (123, 456, 789)", "obfuscated": [ "select * from foo where bar IN (?, ?, ?)" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "mssql" ] }, { "name": "in_clause_strings", "sql": "select * from foo where bar IN ('asdf', 'fdsa')", "obfuscated": [ "select * from foo where bar IN (?, ?)" ], "dialects": [ "mysql", "postgres", "oracle", "cassandra", "mssql" ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/sql_parsing.json000066400000000000000000000150041510742411500265630ustar00rootroot00000000000000[ {"input":"SELECT * FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT F FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT Ff FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT I FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT FROMM FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM foobar WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM `foobar`", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM `foobar` WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM database.foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM database.foobar WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM `database`.foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM `database`.foobar WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM database.`foobar`", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM database.`foobar` WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM (foobar)", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM(foobar)", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM (foobar) WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM (`foobar`)", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM (`foobar`) WHERE x > y", "operation":"select", "table":"foobar"}, {"input":"SELECT * FROM (SELECT * FROM foobar)", "operation":"select", "table":"(subquery)"}, {"input":"SELECT * FROM (SELECT * FROM foobar) WHERE x > y", "operation":"select", "table":"(subquery)"}, {"input":"SELECT xy,zz,y FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT xy,zz,y FROM foobar ORDER BY zy", "operation":"select", "table":"foobar"}, {"input":"SELECT xy,zz,y FROM `foobar`", "operation":"select", "table":"foobar"}, {"input":"SELECT xy,zz,y FROM `foobar` ORDER BY zy", "operation":"select", "table":"foobar"}, {"input":"SELECT `xy`,`zz`,y FROM foobar", "operation":"select", "table":"foobar"}, {"input":"SELECT Name FROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT frok FROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT irom FROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT\n\nirom\n\nFROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT\n\t irom\n FROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT fromm FROM `world`.`City` WHERE Population > ?", "operation":"select", "table":"City"}, {"input":"SELECT * FROM foo,bar", "operation":"select", "table":"foo"}, {"input":" \tSELECT * from \"foo\" WHERE a = b", "operation":"select", "table":"foo"}, {"input":" \tSELECT * \t from \"bar\" WHERE a = b", "operation":"select", "table":"bar"}, {"input":"SELECT * FROM(SELECT * FROM foobar) WHERE x > y", "operation":"select", "table":"(subquery)"}, {"input":"SELECT FROM_UNIXTIME() from \"bar\"", "operation":"select", "table":"bar"}, {"input":"SELECT ffrom from \"frome\"", "operation":"select", "table":"frome"}, {"input":"SELECT ffrom from (\"frome\")", "operation":"select", "table":"frome"}, {"input":"UPDATE abc SET x=1, y=2", "operation":"update", "table":"abc"}, {"input":"UPDATE\nabc\nSET x=1, y=2", "operation":"update", "table":"abc"}, {"input":" \tUPDATE abc SET ffrom='iinto'", "operation":"update", "table":"abc"}, {"input":" \tUPDATE 'abc' SET ffrom='iinto'", "operation":"update", "table":"abc"}, {"input":" \tUPDATE `abc` SET ffrom='iinto'", "operation":"update", "table":"abc"}, {"input":" \tUPDATE \"abc\" SET ffrom='iinto'", "operation":"update", "table":"abc"}, {"input":" \tUPDATE\r\tabc SET ffrom='iinto'", "operation":"update", "table":"abc"}, {"input":"INSERT INTO foobar (x,y) VALUES (1,2)", "operation":"insert", "table":"foobar"}, {"input":"INSERT\nINTO\nfoobar (x,y) VALUES (1,2)", "operation":"insert", "table":"foobar"}, {"input":"INSERT INTO foobar(x,y) VALUES (1,2)", "operation":"insert", "table":"foobar"}, {"input":" /* a */ SELECT * FROM alpha", "operation":"select", "table":"alpha"}, {"input":"SELECT /* a */ * FROM alpha", "operation":"select", "table":"alpha"}, {"input":"SELECT\n/* a */ *\nFROM alpha", "operation":"select", "table":"alpha"}, {"input":"SELECT * /* a */ FROM alpha", "operation":"select", "table":"alpha"}, {"input":"SELECT * FROM /* a */ alpha", "operation":"select", "table":"alpha"}, {"input":"/* X */ SELECT /* Y */ foo/**/ FROM /**/alpha/**/", "operation":"select", "table":"alpha"}, {"input":"mystoredprocedure'123'", "operation":"other", "table":null}, {"input":"mystoredprocedure\t'123'", "operation":"other", "table":null}, {"input":"mystoredprocedure\r'123'", "operation":"other", "table":null}, {"input":"[mystoredprocedure]123", "operation":"other", "table":null}, {"input":"\"mystoredprocedure\"abc", "operation":"other", "table":null}, {"input":"mystoredprocedure", "operation":"other", "table":null} ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/synthetics/000077500000000000000000000000001510742411500255435ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/synthetics/README.md000066400000000000000000000105411510742411500270230ustar00rootroot00000000000000# Synthetics Tests The Synthetics tests are designed to verify that the agent handles valid and invalid Synthetics requests. Each test should run a simulated web transaction. A Synthetics HTTP request header is added to the incoming request at the beginning of a web transaction. During the course of the web transaction, an external request is made. And, at the completion of the web transaction, both a Transaction Trace and Transaction Event are recorded. Each test then verifies that the correct attributes are added to the Transaction Trace and Transaction Event, and the proper request header is added to the external request when required. Or, in the case of an invalid Synthetics request, that the attributes and request header are **not** added. ## Name | Name | Meaning | | ---- | ------- | | `name` | A human-meaningful name for the test case. | ## Settings The `settings` hash contains a number of key-value pairs that the agent will need to use for configuration for the test. | Name | Meaning | | ---- | ------- | | `agentEncodingKey`| The encoding key used by the agent for deobfuscation of the Synthetics request header. | | `syntheticsEncodingKey` | The encoding key used by Synthetics to obfuscate the Synthetics request header. In most tests, `agentEncodingKey` and `syntheticsEncodingKey` are the same. | | `transactionGuid` | The GUID of the simulated transaction. In a non-simulated transaction, this will be randomly generated. But, for testing purposes, you should assign this value as the GUID, since the tests will check for this value to be set in the `nr.guid` attribute of the Transaction Event. | | `trustedAccountIds` | A list of accounts ids that the agent trusts. If the Synthetics request contains a non-trusted account id, it is an invalid request.| ## Inputs The input for each test is a Synthetics request header. The test fixture file shows both the de-obfuscated version of the payload, as well as the resulting obfuscated version. | Name | Meaning | | ---- | ------- | | `inputHeaderPayload` | A decoded form of the contents of the `X-NewRelic-Synthetics` request header. | | `inputObfuscatedHeader` | An obfuscated form of the `X-NewRelic-Synthetics` request header. If you obfuscate `inputHeaderPayload` using the `syntheticsEncodingKey`, this should be the output. | ## Outputs There are three different outputs that are tested for: Transaction Trace, Transaction Event, and External Request Header. ### outputTransactionTrace The `outputTransactionTrace` hash contains three objects: | Name | Meaning | | ---- | ------- | | `header` | The last field of the transaction sample array should be set to the Synthetics Resource ID for a Synthetics request, and should be set to `null` if it isn't. (The last field in the array is the 10th element in the header array, but is `header[9]` in zero-based array notation, so the key name is `field_9`.) | | `expectedIntrinsics` | A set of key-value pairs that represent the attributes that should be set in the intrinsics section of the Transaction Trace. **Note**: If the agent has not implemented the Agent Attributes spec, then the agent should save the attributes in the `Custom` section, and the attribute names should have 'nr.' prepended to them. Read the spec for details. For agents in this situation, they will need to adjust the expected output of the tests accordingly. | | `nonExpectedIntrinsics` | An array of names that represent the attributes that should **not** be set in the intrinsics section of the Transaction Trace.| ### outputTransactionEvent The `outputTransactionEvent` hash contains two objects: | Name | Meaning | | ---- | ------- | | `expectedAttributes` | A set of key-value pairs that represent the attributes that should be set in the `Intrinsic` hash of the Transaction Event. | | `nonExpectedAttributes` | An array of names that represent the attributes that should **not** be set in the `Intrinsic` hash of the Transaction Event. | ### outputExternalRequestHeader The `outputExternalRequestHeader` hash contains two objects: | Name | Meaning | | ---- | ------- | | `expectedHeader` | The outbound header that should be added to external requests (similar to the CAT header), when the original request was made from a valid Synthetics request. | | `nonExpectedHeader` | The outbound header that should **not** be added to external requests, when the original request was made from a non-Synthetics request. | go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/synthetics/synthetics.json000066400000000000000000000226421510742411500306410ustar00rootroot00000000000000[ { "name": "valid_synthetics_request", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 1, 444, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSbA==" }, "outputTransactionTrace": { "header": { "field_9": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr" }, "expectedIntrinsics": { "synthetics_resource_id": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "synthetics_job_id": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "synthetics_monitor_id": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" }, "nonExpectedIntrinsics": [] }, "outputTransactionEvent": { "expectedAttributes": { "nr.guid": "9323dc260548ed0e", "nr.syntheticsResourceId": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "nr.syntheticsJobId": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "nr.syntheticsMonitorId": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" }, "nonExpectedAttributes": [] }, "outputExternalRequestHeader": { "expectedHeader": { "X-NewRelic-Synthetics": "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSbA==" }, "nonExpectedHeader": [] } }, { "name": "non_synthetics_request", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [], "inputObfuscatedHeader": {}, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } }, { "name": "invalid_synthetics_request_unsupported_version", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 777, 444, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agUEAxkCAwwVEkNAQUZHREUVS0JDQB4FBwUDFUtCQ0AeRkdERUpLQkNAQUZHFBsaU1pbWFleXxtdUlNaHAMBBwEbXVJTWhxYWV5fXF1SU1pbWFkWGRRaVVRdXF9eGVhbWlUUAQMBBxlYW1pVFF1cX15ZWFtaVVRdXBBu" }, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } }, { "name": "invalid_synthetics_request_untrusted_account_id", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 1, 999, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agMfDQwPGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSbA==" }, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } }, { "name": "invalid_synthetics_request_mismatched_encoding_key", "settings": { "agentEncodingKey": "0000000000000000000000000000000000000000", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 1, 444, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSbA==" }, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } }, { "name": "invalid_synthetics_request_too_few_header_elements", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 1, 444, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEG4=" }, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } }, { "name": "invalid_synthetics_request_too_many_header_elements", "settings": { "agentEncodingKey": "1234567890123456789012345678901234567890", "syntheticsEncodingKey": "1234567890123456789012345678901234567890", "transactionGuid": "9323dc260548ed0e", "trustedAccountIds": [ 444 ] }, "inputHeaderPayload": [ 1, 444, "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm", "this doesn't belong here" ], "inputObfuscatedHeader": { "X-NewRelic-Synthetics": "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSHRBHXFxFF1xWVUJcFEAVVFJUVl5WEltRR1MVZQ==" }, "outputTransactionTrace": { "header": { "field_9": null }, "expectedIntrinsics": {}, "nonExpectedIntrinsics": [ "synthetics_resource_id", "synthetics_job_id", "synthetics_monitor_id" ] }, "outputTransactionEvent": { "expectedAttributes": {}, "nonExpectedAttributes": [ "nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId" ] }, "outputExternalRequestHeader": { "expectedHeader": {}, "nonExpectedHeader": [ "X-NewRelic-Synthetics" ] } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/transaction_segment_terms.json000066400000000000000000000222601510742411500315240ustar00rootroot00000000000000[ { "testname": "basic", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom", "terms": ["one", "two", "three"] }, { "prefix": "WebTransaction/Uri", "terms": ["seven", "eight", "nine"] } ], "tests": [ { "input": "WebTransaction/Uri/one/two/seven/user/nine/account", "expected": "WebTransaction/Uri/*/seven/*/nine/*" }, { "input": "WebTransaction/Custom/one/two/seven/user/nine/account", "expected": "WebTransaction/Custom/one/two/*" }, { "input": "WebTransaction/Other/one/two/foo/bar", "expected": "WebTransaction/Other/one/two/foo/bar" } ] }, { "testname": "prefix_with_trailing_slash", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom/", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Custom/a/b/c", "expected": "WebTransaction/Custom/a/b/*" }, { "input": "WebTransaction/Other/a/b/c", "expected": "WebTransaction/Other/a/b/c" } ] }, { "testname": "prefix_with_trailing_spaces_and_then_slash", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom /", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Custom /a/b/c", "expected": "WebTransaction/Custom /a/b/*" }, { "input": "WebTransaction/Custom /a/b/c", "expected": "WebTransaction/Custom /a/b/c" }, { "input": "WebTransaction/Custom/a/b/c", "expected": "WebTransaction/Custom/a/b/c" } ] }, { "testname": "prefix_with_trailing_spaces", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom ", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Custom /a/b/c", "expected": "WebTransaction/Custom /a/b/*" }, { "input": "WebTransaction/Custom /a/b/c", "expected": "WebTransaction/Custom /a/b/c" }, { "input": "WebTransaction/Custom/a/b/c", "expected": "WebTransaction/Custom/a/b/c" } ] }, { "testname": "overlapping_prefix_last_one_only_applied", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["one", "two", "three"] }, { "prefix": "WebTransaction/Foo", "terms": ["one", "two", "zero"] } ], "tests": [ { "input": "WebTransaction/Foo/zero/one/two/three/four", "expected": "WebTransaction/Foo/zero/one/two/*" } ] }, { "testname": "terms_are_order_independent", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["one", "two", "three"] } ], "tests": [ { "input": "WebTransaction/Foo/bar/one/three/two", "expected": "WebTransaction/Foo/*/one/three/two" }, { "input": "WebTransaction/Foo/three/one/one/two/three", "expected": "WebTransaction/Foo/three/one/one/two/three" } ] }, { "testname": "invalid_rule_not_enough_prefix_segments", "transaction_segment_terms": [ { "prefix": "WebTransaction", "terms": ["one", "two"] } ], "tests": [ { "input": "WebTransaction/Foo/bar/one/three/two", "expected": "WebTransaction/Foo/bar/one/three/two" }, { "input": "WebTransaction/Foo/three/one/one/two/three", "expected": "WebTransaction/Foo/three/one/one/two/three" } ] }, { "testname": "invalid_rule_not_enough_prefix_segments_ending_in_slash", "transaction_segment_terms": [ { "prefix": "WebTransaction/", "terms": ["one", "two"] } ], "tests": [ { "input": "WebTransaction/Foo/bar/one/three/two", "expected": "WebTransaction/Foo/bar/one/three/two" }, { "input": "WebTransaction/Foo/three/one/one/two/three", "expected": "WebTransaction/Foo/three/one/one/two/three" } ] }, { "testname": "invalid_rule_too_many_prefix_segments", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo/bar", "terms": ["one", "two"] } ], "tests": [ { "input": "WebTransaction/Foo/bar/one/three/two", "expected": "WebTransaction/Foo/bar/one/three/two" }, { "input": "WebTransaction/Foo/three/one/one/two/three", "expected": "WebTransaction/Foo/three/one/one/two/three" } ] }, { "testname": "invalid_rule_prefix_with_trailing_slash_and_then_space", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom/ ", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Custom/a/b/c", "expected": "WebTransaction/Custom/a/b/c" } ] }, { "testname": "invalid_rule_prefix_with_multiple_trailing_slashes", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom////", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Custom/a/b/c", "expected": "WebTransaction/Custom/a/b/c" } ] }, { "testname": "invalid_rule_null_prefix", "transaction_segment_terms": [ { "terms": ["one", "two", "three"] } ], "tests": [ { "input": "WebTransaction/Custom/one/two/seven/user/nine/account", "expected": "WebTransaction/Custom/one/two/seven/user/nine/account" } ] }, { "testname": "invalid_rule_null_terms", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom" } ], "tests": [ { "input": "WebTransaction/Custom/one/two/seven/user/nine/account", "expected": "WebTransaction/Custom/one/two/seven/user/nine/account" } ] }, { "testname": "empty_terms", "transaction_segment_terms": [ { "prefix": "WebTransaction/Custom", "terms": [] } ], "tests": [ { "input": "WebTransaction/Custom/one/two/seven/user/nine/account", "expected": "WebTransaction/Custom/*" }, { "input": "WebTransaction/Custom/", "expected": "WebTransaction/Custom/" }, { "input": "WebTransaction/Custom", "expected": "WebTransaction/Custom" } ] }, { "testname": "two_segment_transaction_name", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["a", "b", "c"] } ], "tests": [ { "input": "WebTransaction/Foo", "expected": "WebTransaction/Foo" } ] }, { "testname": "two_segment_transaction_name_with_trailing_slash", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["a", "b", "c"] } ], "tests": [ { "input": "WebTransaction/Foo/", "expected": "WebTransaction/Foo/" } ] }, { "testname": "transaction_segment_with_adjacent_slashes", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["a", "b", "c"] } ], "tests": [ { "input": "WebTransaction/Foo///a/b///c/d/", "expected": "WebTransaction/Foo/*/a/b/*/c/*" }, { "input": "WebTransaction/Foo///a/b///c///", "expected": "WebTransaction/Foo/*/a/b/*/c/*" } ] }, { "testname": "transaction_name_with_single_segment", "transaction_segment_terms": [ { "prefix": "WebTransaction/Foo", "terms": ["a", "b", "c"] } ], "tests": [ { "input": "WebTransaction", "expected": "WebTransaction" } ] }, { "testname": "prefix_must_match_first_two_segments", "transaction_segment_terms": [ { "prefix": "WebTransaction/Zip", "terms": ["a", "b"] } ], "tests": [ { "input": "WebTransaction/Zip/a/b/c", "expected": "WebTransaction/Zip/a/b/*" }, { "input": "WebTransaction/ZipZap/a/b/c", "expected": "WebTransaction/ZipZap/a/b/c" } ] }, { "testname": "one_bad_rule_does_not_scrap_all_rules", "transaction_segment_terms": [ { "prefix": "WebTransaction/MissingTerms" }, { "prefix": "WebTransaction/Uri", "terms": ["seven", "eight", "nine"] } ], "tests": [ { "input": "WebTransaction/Uri/one/two/seven/user/nine/account", "expected": "WebTransaction/Uri/*/seven/*/nine/*" } ] }, { "testname": "one_bad_matching_rule_at_end_does_not_scrap_other_matching_rules", "transaction_segment_terms": [ { "prefix": "WebTransaction/Uri", "terms": ["seven", "eight", "nine"] }, { "prefix": "WebTransaction/Uri" } ], "tests": [ { "input": "WebTransaction/Uri/one/two/seven/user/nine/account", "expected": "WebTransaction/Uri/*/seven/*/nine/*" } ] } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/url_clean.json000066400000000000000000000026561510742411500262160ustar00rootroot00000000000000[ {"testname":"only domain", "expected":"domain.com", "input":"domain.com"}, {"testname":"domain path", "expected":"domain.com/a/b/c", "input":"domain.com/a/b/c"}, {"testname":"port", "expected":"domain.com:1234/a/b/c", "input":"domain.com:1234/a/b/c"}, {"testname":"user", "expected":"domain.com/a/b/c", "input":"user@domain.com/a/b/c"}, {"testname":"user pw", "expected":"domain.com/a/b/c", "input":"user:password@domain.com/a/b/c"}, {"testname":"scheme domain", "expected":"p://domain.com", "input":"p://domain.com"}, {"testname":"scheme path", "expected":"p://domain.com/a/b/c", "input":"p://domain.com/a/b/c"}, {"testname":"scheme port", "expected":"p://domain.com:1234/a/b/c", "input":"p://domain.com:1234/a/b/c"}, {"testname":"scheme user", "expected":"p://domain.com/a/b/c", "input":"p://user@domain.com/a/b/c"}, {"testname":"scheme user pw", "expected":"p://domain.com/a/b/c", "input":"p://user:password@domain.com/a/b/c"}, {"testname":"fragment", "expected":"p://domain.com/a/b/c", "input":"p://user:password@domain.com/a/b/c#fragment"}, {"testname":"query", "expected":"p://domain.com/a/b/c", "input":"p://user:password@domain.com/a/b/c?query=yes"}, {"testname":"semi-colon", "expected":"p://domain.com/a/b/c", "input":"p://user:password@domain.com/a/b/c;semi=yes"} ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/url_domain_extraction.json000066400000000000000000000034711510742411500306370ustar00rootroot00000000000000[ {"expected":"domain", "input":"scheme://domain:port/path?query_string#fragment_id"}, {"expected":"0.0.0.0", "input":"scheme://0.0.0.0:port/path?query_string#fragment_id"}, {"expected":"localhost", "input":"scheme://localhost:port/path?query_string#fragment_id"}, {"expected":"127.0.0.1", "input":"scheme://127.0.0.1:port/path?query_string#fragment_id"}, {"expected":"0.0.0.0", "input":"scheme://0.0.0.0:8087/path?query_string#fragment_id"}, {"expected":"localhost", "input":"scheme://localhost:8087/path?query_string#fragment_id"}, {"expected":"127.0.0.1", "input":"scheme://127.0.0.1:8087/path?query_string#fragment_id"}, {"expected":"a.b", "input":"a.b"}, {"expected":"a.b", "input":"user@a.b"}, {"expected":"a.b", "input":"user:pass@a.b"}, {"expected":"a.b", "input":"a.b:123"}, {"expected":"a.b", "input":"user@a.b:123"}, {"expected":"a.b", "input":"user:pass@a.b:123"}, {"expected":"a.b", "input":"a.b/c/d?e=f"}, {"expected":"a.b", "input":"user@a.b/c/d?e=f"}, {"expected":"a.b", "input":"user:pass@a.b/c/d?e=f"}, {"expected":"a.b", "input":"a.b:123/c/d?e=f"}, {"expected":"a.b", "input":"user@a.b:123/c/d?e=f"}, {"expected":"a.b", "input":"user:pass@a.b:123/c/d?e=f"}, {"expected":"a.b", "input":"p://a.b"}, {"expected":"a.b", "input":"p://user@a.b"}, {"expected":"a.b", "input":"p://user:pass@a.b"}, {"expected":"a.b", "input":"p://a.b:123"}, {"expected":"a.b", "input":"p://user@a.b:123"}, {"expected":"a.b", "input":"p://user:pass@a.b:123"}, {"expected":"a.b", "input":"p://a.b/c/d?e=f"}, {"expected":"a.b", "input":"p://user@a.b/c/d?e=f"}, {"expected":"a.b", "input":"p://user:pass@a.b/c/d?e=f"}, {"expected":"a.b", "input":"p://a.b:123/c/d?e=f"}, {"expected":"a.b", "input":"p://user@a.b:123/c/d?e=f"}, {"expected":"a.b", "input":"p://user:pass@a.b:123/c/d?e=f"} ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization/000077500000000000000000000000001510742411500257215ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization/README.md000066400000000000000000000053231510742411500272030ustar00rootroot00000000000000# The Utilization Tests The Utilization tests ensure that the appropriate information is being gathered for pricing. It is centered around ensuring that the JSON generated by all agents is correct. Each JSON block is a test case, with potentially the following fields: - `testname`: The name of the test. - `input_total_ram_mib`: The total ram number calculated by the agent. - `input_logical_processors`: The number of logical processors calculated by the agent. - `input_hostname`: The `hostname` calculated by the agent. - `input_full_hostname`: The `full_hostname` calculated by the agent. - `input_ip_address`: A string array containing all the values in `ip_address` calculated by the agent. - `input_aws_id`: The aws `id` determined by the agent. - `input_aws_type`: The aws `type` determined by the agent. - `input_aws_zone`: The aws `zone` determined by the agent. - `input_environment_variables`: Any environment variables which have been set. - `expected_output_json`: The expected JSON output from the agent for the `utilization` hash. New fields for Google Cloud Platform (gcp), Pivotal Cloud Foundry (pcf), and Azure added as of [Utilization spec version 8](https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md): - `input_gcp_id`: The gcp `id` determined by the agent. - `input_gcp_type`: The gcp `machineType` determined by the agent. - `input_gcp_name`: The gcp `name` determined by the agent. - `input_gcp_zone`: The gcp `zone` determined by the agent. - `input_pcf_guid`: The pcf `cf_instance_guid` determined by the agent. - `input_pcf_ip`: The pcf `cf_instance_ip` determined by the agent. - `input_pcf_mem_limit`: The pcf `memory_limit` determined by the agent. - `input_azure_location`: The azure `location` determined by the agent. - `input_azure_name`: The azure `name` determined by the agent. - `input_azure_id`: The azure `vmId` determined by the agent. - `input_azure_size`: The azure `vmSize` determined by the agent. Test cases for `boot_id.json` added as of [Utilization spec version 8](https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md): - `testname`: The name of the test. - `input_total_ram_mib`: The total ram number calculated by the agent. - `input_logical_processors`: The number of logical processors calculated by the agent. - `input_hostname`: The hostname calculated by the agent. - `input_boot_id`: The `boot_id` determined by the agent. - `expected_output_json`: The expected JSON output from the agent for the utilization hash. - `expected_metrics`: Supportability metrics that are either expected or unexpected in a given case. If the `call_count` is 0 it should be asserted that the Supportability metric was not sent. go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization/boot_id.json000066400000000000000000000063621510742411500302420ustar00rootroot00000000000000[ { "testname": "boot_id file not found", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": null, "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost" }, "expected_metrics": { "Supportability/utilization/boot_id/error": { "call_count": 1 } } }, { "testname": "valid boot_id, should be 36 characters", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": "8e84c4ab-943d-46c2-8675-fdf0ab61e1c4", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost", "boot_id": "8e84c4ab-943d-46c2-8675-fdf0ab61e1c4" } }, { "testname": "boot_id too long, should be truncated to 128 characters max", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost", "boot_id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }, "expected_metrics": { "Supportability/utilization/boot_id/error": { "call_count": 1 } } }, { "testname": "boot_id too short, should be reported as is", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": "1234", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost", "boot_id": "1234" }, "expected_metrics": { "Supportability/utilization/boot_id/error": { "call_count": 1 } } }, { "testname": "boot_id too short with non-alphanumeric characters, should be reported as is", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": "", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost", "boot_id": "" }, "expected_metrics": { "Supportability/utilization/boot_id/error": { "call_count": 1 } } }, { "testname": "boot_id file empty", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_boot_id": "", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost" }, "expected_metrics": { "Supportability/utilization/boot_id/error": { "call_count": 1 } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization/utilization_json.json000066400000000000000000000272261510742411500322310ustar00rootroot00000000000000[ { "testname": "only agent derived data", "input_total_ram_mib": 1024, "input_logical_processors": 8, "input_hostname": "myhost", "input_full_hostname": "myhost.com", "input_ip_address": ["1.2.3.4", "1.2.3.5"], "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 1024, "hostname": "myhost", "full_hostname": "myhost.com", "ip_address": ["1.2.3.4", "1.2.3.5"] } }, { "testname": "only agent derived but bad data", "input_total_ram_mib": null, "input_logical_processors": null, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"], "expected_output_json": { "metadata_version": 5, "logical_processors": null, "total_ram_mib": null, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"] } }, { "testname": "agent derived null and some environment variables", "input_total_ram_mib": null, "input_logical_processors": null, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["::FFFF:129.144.52.38"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": 8, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": 2048 }, "expected_output_json": { "metadata_version": 5, "logical_processors": null, "total_ram_mib": null, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["::FFFF:129.144.52.38"], "config": { "logical_processors": 8, "total_ram_mib": 2048 } } }, { "testname": "all environment variables", "input_total_ram_mib": 1, "input_logical_processors": 2, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["8.8.8.8"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": 16, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": 4096, "NEW_RELIC_UTILIZATION_BILLING_HOSTNAME": "localhost" }, "expected_output_json": { "metadata_version": 5, "logical_processors": 2, "total_ram_mib": 1, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["8.8.8.8"], "config": { "logical_processors": 16, "total_ram_mib": 4096, "hostname": "localhost" } } }, { "testname": "all environment variables with error in processors", "input_total_ram_mib": 1024, "input_logical_processors": 4, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": "abc", "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": 4096, "NEW_RELIC_UTILIZATION_BILLING_HOSTNAME": "localhost" }, "expected_output_json": { "metadata_version": 5, "logical_processors": 4, "total_ram_mib": 1024, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"], "config": { "total_ram_mib": 4096, "hostname": "localhost" } } }, { "testname": "all environment variables with error in ram", "input_total_ram_mib": 1024, "input_logical_processors": 4, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": 8, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": "notgood", "NEW_RELIC_UTILIZATION_BILLING_HOSTNAME": "localhost" }, "expected_output_json": { "metadata_version": 5, "logical_processors": 4, "total_ram_mib": 1024, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["2001:0db8:0000:0042:0000:8a2e:0370:7334"], "config": { "logical_processors": 8, "hostname": "localhost" } } }, { "testname": "only agent derived data with aws", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_aws_id": "8BADFOOD", "input_aws_type": "t2.micro", "input_aws_zone": "us-west-1", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"], "vendors": { "aws": { "instanceId": "8BADFOOD", "instanceType": "t2.micro", "availabilityZone": "us-west-1" } } } }, { "testname": "invalid agent derived data with aws", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_aws_id": null, "input_aws_type": "t2.micro", "input_aws_zone": "us-west-1", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"] } }, { "testname": "only agent derived data with gcp", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_gcp_id": "3161347020215157000", "input_gcp_type": "projects/492690098729/machineTypes/custom-1-1024", "input_gcp_name": "aef-default-20170501t160547-7gh8", "input_gcp_zone": "projects/492690098729/zones/us-central1-c", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"], "vendors": { "gcp": { "id": "3161347020215157000", "machineType": "custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "us-central1-c" } } } }, { "testname": "invalid agent derived data with gcp", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_gcp_id": "3161347020215157000", "input_gcp_type": "projects/492690098729/machineTypes/custom-1-1024", "input_gcp_name": null, "input_gcp_zone": "projects/492690098729/zones/us-central1-c", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"] } }, { "testname": "only agent derived data with pcf", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_pcf_guid": "b977d090-83db-4bdb-793a-bb77", "input_pcf_ip": "10.10.147.130", "input_pcf_mem_limit": "1024m", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"], "vendors": { "pcf": { "cf_instance_guid": "b977d090-83db-4bdb-793a-bb77", "cf_instance_ip": "10.10.147.130", "memory_limit": "1024m" } } } }, { "testname": "invalid agent derived data with pcf", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_pcf_guid": null, "input_pcf_ip": "10.10.147.130", "input_pcf_mem_limit": "1024m", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"] } }, { "testname": "only agent derived data with azure", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_azure_location": "CentralUS", "input_azure_name": "IMDSCanary", "input_azure_id": "5c08b38e-4d57-4c23-ac45-aca61037f084", "input_azure_size": "Standard_DS2", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"], "vendors": { "azure": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" } } } }, { "testname": "invalid agent derived data with azure", "input_total_ram_mib": 2048, "input_logical_processors": 8, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["1.2.3.4"], "input_azure_location": "CentralUS", "input_azure_name": "IMDSCanary", "input_azure_id": null, "input_azure_size": "Standard_DS2", "expected_output_json": { "metadata_version": 5, "logical_processors": 8, "total_ram_mib": 2048, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["1.2.3.4"] } }, { "testname": "kubernetes service host environment variable", "input_total_ram_mib": null, "input_logical_processors": null, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["::FFFF:129.144.52.38"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": 8, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": 2048, "KUBERNETES_SERVICE_HOST": "10.96.0.1" }, "expected_output_json": { "metadata_version": 5, "logical_processors": null, "total_ram_mib": null, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["::FFFF:129.144.52.38"], "config": { "logical_processors": 8, "total_ram_mib": 2048 }, "vendors": { "kubernetes": { "kubernetes_service_host": "10.96.0.1" } } } }, { "testname": "only kubernetes service port environment variable", "input_total_ram_mib": null, "input_logical_processors": null, "input_hostname": "myotherhost", "input_full_hostname": "myotherhost.com", "input_ip_address": ["::FFFF:129.144.52.38"], "input_environment_variables": { "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": 8, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": 2048, "KUBERNETES_SERVICE_PORT": "8080" }, "expected_output_json": { "metadata_version": 5, "logical_processors": null, "total_ram_mib": null, "hostname": "myotherhost", "full_hostname": "myotherhost.com", "ip_address": ["::FFFF:129.144.52.38"], "config": { "logical_processors": 8, "total_ram_mib": 2048 } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/000077500000000000000000000000001510742411500311435ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/README.md000066400000000000000000000020331510742411500324200ustar00rootroot00000000000000# Vendor Specific Utilization Tests The Utilization tests ensure that the appropriate information is being gathered for pricing for a particular cloud vendor. It is centered around ensuring that the JSON generated by all agents is correct. Each JSON block is a test case, with potentially the following fields: - `testname`: The name of the test. - `uri`: The API endpoint for the cloud vendor. This contains a response indicating what the expected return from the API is for a given test. - `expected_vendors_hash`: The vendor hash that should be generated by the agent based on the uri response. - `expected_metrics`: Supportability metrics that are either expected or unexpected in a given case. If the `call_count` is 0 it should be asserted that the Supportability metric was not sent. As of [Metadata version 3](https://source.datanerd.us/agents/agent-specs/blob/c78cddeaa5fa23dce892b8c6da95b9f900636c35/Utilization.md) specs have been added for Azure, Google Cloud Platform, and Pivotal Cloud Foundry in addition to updates for AWS.go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/aws.json000066400000000000000000000162651510742411500326420ustar00rootroot00000000000000[ { "testname": "aws api times out, no vendor hash or supportability metric reported", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": null, "instanceType": null, "availabilityZone": null }, "timeout": true } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 0 } } }, { "testname": "instance type, instance-id, availability-zone are all happy", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "i-test.19characters", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": { "aws": { "instanceId": "i-test.19characters", "instanceType": "test.type", "availabilityZone": "us-west-2b" } } }, { "testname": "instance type with invalid characters", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "test.id", "instanceType": "", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "instance type too long", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "test.id", "instanceType": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "instance id with invalid characters", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "instance id too long", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "availability zone with invalid characters", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "test.id", "instanceType": "test.type", "availabilityZone": "" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "availability zone too long", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "test.id", "instanceType": "test.type", "availabilityZone": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "UTF-8 high codepoints", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": { "aws": { "instanceId": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "instanceType": "test.type", "availabilityZone": "us-west-2b" } } }, { "testname": "comma with multibyte characters", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲, 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "Exclamation point response", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "bang!", "instanceType": "test.type", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/aws/error": { "call_count": 1 } } }, { "testname": "Valid punctuation in response", "uri": { "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document": { "response": { "instanceId": "test.id", "instanceType": "a-b_c.3... and/or 503 867-5309", "availabilityZone": "us-west-2b" }, "timeout": false } }, "expected_vendors_hash": { "aws": { "instanceId": "test.id", "instanceType": "a-b_c.3... and/or 503 867-5309", "availabilityZone": "us-west-2b" } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/azure.json000066400000000000000000000213511510742411500331660ustar00rootroot00000000000000[ { "testname": "azure api times out, no vendor hash or supportability metric reported", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": null, "name": null, "vmId": null, "vmSize": null }, "timeout": true } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 0 } } }, { "testname": "vmId, location, name, vmSize are all happy", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": { "azure": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" } } }, { "testname": "vmSize with invalid characters", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "vmSize too long", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "vmId with invalid characters", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "vmId too long", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "location with invalid characters", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "location too long", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "name with invalid characters", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "name too long", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "UTF-8 high codepoints", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": { "azure": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "vmSize": "Standard_DS2" } } }, { "testname": "comma with multibyte characters", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲, 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "Exclamation point in response", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "Bang!", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "Standard_DS2" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/azure/error": { "call_count": 1 } } }, { "testname": "Valid punctuation in response", "uri": { "http://169.254.169.254/metadata/instance/compute?api-version=2017-03-01": { "response": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "a-b_c.3... and/or 503 867-5309" }, "timeout": false } }, "expected_vendors_hash": { "azure": { "location": "CentralUS", "name": "IMDSCanary", "vmId": "5c08b38e-4d57-4c23-ac45-aca61037f084", "vmSize": "a-b_c.3... and/or 503 867-5309" } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/gcp.json000066400000000000000000000227641510742411500326220ustar00rootroot00000000000000[ { "testname": "gcp api times out, no vendor hash or supportability metric reported", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": null, "machineType": null, "name": null, "zone": null }, "timeout": true } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 0 } } }, { "testname": "machine type, id, zone, name are all happy", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": { "gcp": { "id": "3161347020215157000", "machineType": "custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "us-central1-c" } } }, { "testname": "machine type with invalid characters", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "", "name": "aef-default-20170501t160547-7gh8", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "machine type too long", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "name": "aef-default-20170501t160547-7gh8", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "id with invalid characters", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": "", "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "id too long", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "zone with invalid characters", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "zone too long", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "aef-default-20170501t160547-7gh8", "zone": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "name with invalid characters", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "name too long", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "UTF-8 high codepoints", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": { "gcp": { "id": "3161347020215157000", "machineType": "custom-1-1024", "name": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "zone": "us-central1-c" } } }, { "testname": "comma with multibyte characters", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲, 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "Exclamation point in response", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "Bang!", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/gcp/error": { "call_count": 1 } } }, { "testname": "Valid punctuation in response", "uri": { "http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true": { "response": { "id": 3161347020215157000, "machineType": "projects/492690098729/machineTypes/custom-1-1024", "name": "a-b_c.3... and/or 503 867-5309", "zone": "projects/492690098729/zones/us-central1-c" }, "timeout": false } }, "expected_vendors_hash": { "gcp": { "id": "3161347020215157000", "machineType": "custom-1-1024", "name": "a-b_c.3... and/or 503 867-5309", "zone": "us-central1-c" } } } ] go-agent-3.42.0/v3/internal/crossagent/cross_agent_tests/utilization_vendor_specific/pcf.json000066400000000000000000000165511510742411500326160ustar00rootroot00000000000000[ { "testname": "routine failure to retrieve environment variables, no vendor hash or supportability metric reported", "env_vars": { "CF_INSTANCE_GUID": { "response": null, "timeout": true }, "CF_INSTANCE_IP": { "response": null, "timeout": true }, "MEMORY_LIMIT": { "response": null, "timeout": true } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 0 } } }, { "testname": "cf_instance_guid, cf_instance_ip, memory_limit are all happy", "env_vars": { "CF_INSTANCE_GUID": { "response": "fd326c0e-847e-47a1-65cc-45f6", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": { "pcf": { "cf_instance_guid": "fd326c0e-847e-47a1-65cc-45f6", "cf_instance_ip": "10.10.149.48", "memory_limit": "1024m" } } }, { "testname": "cf_instance_guid with invalid characters", "env_vars": { "CF_INSTANCE_GUID": { "response": "", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "cf_instance_guid too long", "env_vars": { "CF_INSTANCE_GUID": { "response": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "cf_instance_ip with invalid characters", "env_vars": { "CF_INSTANCE_GUID": { "response": "fd326c0e-847e-47a1-65cc-45f6", "timeout": false }, "CF_INSTANCE_IP": { "response": "", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "cf_instance_ip too long", "env_vars": { "CF_INSTANCE_GUID": { "response": "fd326c0e-847e-47a1-65cc-45f6", "timeout": false }, "CF_INSTANCE_IP": { "response": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "memory_limit with invalid characters", "env_vars": { "CF_INSTANCE_GUID": { "response": "fd326c0e-847e-47a1-65cc-45f6", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "memory_limit too long", "env_vars": { "CF_INSTANCE_GUID": { "response": "fd326c0e-847e-47a1-65cc-45f6", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "UTF-8 high codepoints", "env_vars": { "CF_INSTANCE_GUID": { "response": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": { "pcf": { "cf_instance_guid": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "cf_instance_ip": "10.10.149.48", "memory_limit": "1024m" } } }, { "testname": "comma with multibyte characters", "env_vars": { "CF_INSTANCE_GUID": { "response": "滈 橀槶澉 鞻饙騴 鱙鷭黂 甗糲, 紁羑 嗂 蛶觢豥 餤駰鬳 釂鱞鸄", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "Exclamation point in response", "env_vars": { "CF_INSTANCE_GUID": { "response": "Bang!", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": null, "expected_metrics": { "Supportability/utilization/pcf/error": { "call_count": 1 } } }, { "testname": "Valid punctuation in response", "env_vars": { "CF_INSTANCE_GUID": { "response": "a-b_c.3... and/or 503 867-5309", "timeout": false }, "CF_INSTANCE_IP": { "response": "10.10.149.48", "timeout": false }, "MEMORY_LIMIT": { "response": "1024m", "timeout": false } }, "expected_vendors_hash": { "pcf": { "cf_instance_guid": "a-b_c.3... and/or 503 867-5309", "cf_instance_ip": "10.10.149.48", "memory_limit": "1024m" } } } ] go-agent-3.42.0/v3/internal/crossagent/crossagent.go000066400000000000000000000030331510742411500223130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package crossagent import ( "encoding/json" "io/ioutil" "os" "path/filepath" "runtime" ) var ( crossAgentDir = func() string { if s := os.Getenv("NEW_RELIC_CROSS_AGENT_TESTS"); s != "" { return s } _, here, _, _ := runtime.Caller(0) return filepath.Join(filepath.Dir(here), "cross_agent_tests") }() ) // ReadFile reads a file from the crossagent tests directory given as with // ioutil.ReadFile. func ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(filepath.Join(crossAgentDir, name)) } // FileMissing returns true if the cross agent test fixture does not exist. func FileMissing(name string) bool { if _, err := os.Stat(filepath.Join(crossAgentDir, name)); os.IsNotExist(err) { return true } return false } // ReadJSON takes the name of a file and parses it using JSON.Unmarshal into // the interface given. func ReadJSON(name string, v interface{}) error { data, err := ReadFile(name) if err != nil { return err } return json.Unmarshal(data, v) } // ReadDir reads a directory relative to crossagent tests and returns an array // of absolute filepaths of the files in that directory. func ReadDir(name string) ([]string, error) { dir := filepath.Join(crossAgentDir, name) entries, err := ioutil.ReadDir(dir) if err != nil { return nil, err } var files []string for _, info := range entries { if !info.IsDir() { files = append(files, filepath.Join(dir, info.Name())) } } return files, nil } go-agent-3.42.0/v3/internal/expect.go000066400000000000000000000076651510742411500173020ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal // Validator is used for testing. type Validator interface { Error(...interface{}) } // WantMetric is a metric expectation. If Data is nil, then any data values are // acceptable. If Data has len 1, then only the metric count is validated. type WantMetric struct { Name string Scope string Forced interface{} // true, false, or nil Data []float64 } // WantError is a traced error expectation. type WantError struct { TxnName string Msg string Klass string GUID string UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantLog is a traced log event expectation type WantLog struct { Attributes map[string]interface{} Severity string Message string SpanID string TraceID string Timestamp int64 } func uniquePointer() *struct{} { s := struct{}{} return &s } var ( // MatchAnything is for use when matching attributes. MatchAnything = uniquePointer() // MatchAnyString is a placeholder for matching any string MatchAnyString = "xxANY-STRINGxx" // MatchAnyUnixMilli is a placeholder for matching any unix millisecond timestamp int64 MatchAnyUnixMilli = int64(-1) ) // WantEvent is a transaction or error event expectation. type WantEvent struct { Intrinsics map[string]interface{} UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantTxnTrace is a transaction trace expectation. type WantTxnTrace struct { // DurationMillis is compared if non-nil. DurationMillis *float64 MetricName string NumSegments int UserAttributes map[string]interface{} AgentAttributes map[string]interface{} Intrinsics map[string]interface{} // If the Root's SegmentName is populated then the segments will be // tested, otherwise NumSegments will be tested. Root WantTraceSegment } // WantTraceSegment is a transaction trace segment expectation. type WantTraceSegment struct { SegmentName string // RelativeStartMillis and RelativeStopMillis will be tested if they are // provided: This makes it easy for top level tests which cannot // control duration. RelativeStartMillis interface{} RelativeStopMillis interface{} Attributes map[string]interface{} Children []WantTraceSegment } // WantSlowQuery is a slowQuery expectation. type WantSlowQuery struct { Count int32 MetricName string Query string TxnName string TxnURL string DatabaseName string Host string PortPathOrID string Params map[string]interface{} } // HarvestTestinger is implemented by the app. It sets an empty test harvest // and modifies the connect reply if a callback is provided. type HarvestTestinger interface { HarvestTesting(replyfn func(*ConnectReply)) } // HarvestTesting allows integration packages to test instrumentation. func HarvestTesting(app interface{}, replyfn func(*ConnectReply)) { ta, ok := app.(HarvestTestinger) if !ok { panic("HarvestTesting type assertion failure") } ta.HarvestTesting(replyfn) } // WantTxn provides the expectation parameters to ExpectTxnMetrics. type WantTxn struct { Name string IsWeb bool NumErrors int UnknownCaller bool ErrorByCaller bool } // Expect exposes methods that allow for testing whether the correct data was // captured. type Expect interface { ExpectCustomEvents(t Validator, want []WantEvent) ExpectLogEvents(t Validator, want []WantLog) ExpectErrors(t Validator, want []WantError) ExpectErrorEvents(t Validator, want []WantEvent) ExpectTxnEvents(t Validator, want []WantEvent) ExpectMetrics(t Validator, want []WantMetric) ExpectMetricsPresent(t Validator, want []WantMetric) ExpectTxnMetrics(t Validator, want WantTxn) ExpectTxnTraces(t Validator, want []WantTxnTrace) ExpectSlowQueries(t Validator, want []WantSlowQuery) ExpectSpanEvents(t Validator, want []WantEvent) } go-agent-3.42.0/v3/internal/hidden_methods.go000066400000000000000000000024451510742411500207570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import "io" // This file contains interfaces that are implemented by Transaction and // Application but not exposed as public methods so they will only be used in // integration packages. // ServerlessWriter is implemented by newrelic.Application. type ServerlessWriter interface { ServerlessWrite(arn string, writer io.Writer) } // ServerlessWrite exists to avoid type assertion in the nrlambda integration // package. func ServerlessWrite(app interface{}, arn string, writer io.Writer) { if s, ok := app.(ServerlessWriter); ok { s.ServerlessWrite(arn, writer) } } // AddAgentAttributer allows instrumentation to add agent attributes without // exposing a Transaction method. type AddAgentAttributer interface { AddAgentAttribute(name string, stringVal string, otherVal interface{}) } // AddAgentSpanAttributer should be implemented by the Transaction. type AddAgentSpanAttributer interface { AddAgentSpanAttribute(key string, val string) } // AddAgentSpanAttribute allows instrumentation packages to add span attributes. func AddAgentSpanAttribute(txn interface{}, key string, val string) { if aa, ok := txn.(AddAgentSpanAttributer); ok { aa.AddAgentSpanAttribute(key, val) } } go-agent-3.42.0/v3/internal/jsonx/000077500000000000000000000000001510742411500166065ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/jsonx/encode.go000066400000000000000000000110431510742411500203710ustar00rootroot00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package jsonx extends the encoding/json package to encode JSON // incrementally and without requiring reflection. package jsonx import ( "bytes" "encoding/json" "math" "reflect" "strconv" "unicode/utf8" ) var hex = "0123456789abcdef" // AppendString escapes s appends it to buf. func AppendString(buf *bytes.Buffer, s string) { buf.WriteByte('"') start := 0 for i := 0; i < len(s); { if b := s[i]; b < utf8.RuneSelf { if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' { i++ continue } if start < i { buf.WriteString(s[start:i]) } switch b { case '\\', '"': buf.WriteByte('\\') buf.WriteByte(b) case '\n': buf.WriteByte('\\') buf.WriteByte('n') case '\r': buf.WriteByte('\\') buf.WriteByte('r') case '\t': buf.WriteByte('\\') buf.WriteByte('t') default: // This encodes bytes < 0x20 except for \n and \r, // as well as <, > and &. The latter are escaped because they // can lead to security holes when user-controlled strings // are rendered into JSON and served to some browsers. buf.WriteString(`\u00`) buf.WriteByte(hex[b>>4]) buf.WriteByte(hex[b&0xF]) } i++ start = i continue } c, size := utf8.DecodeRuneInString(s[i:]) if c == utf8.RuneError && size == 1 { if start < i { buf.WriteString(s[start:i]) } buf.WriteString(`\ufffd`) i += size start = i continue } // U+2028 is LINE SEPARATOR. // U+2029 is PARAGRAPH SEPARATOR. // They are both technically valid characters in JSON strings, // but don't work in JSONP, which has to be evaluated as JavaScript, // and can lead to security holes there. It is valid JSON to // escape them, so we do so unconditionally. // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. if c == '\u2028' || c == '\u2029' { if start < i { buf.WriteString(s[start:i]) } buf.WriteString(`\u202`) buf.WriteByte(hex[c&0xF]) i += size start = i continue } i += size } if start < len(s) { buf.WriteString(s[start:]) } buf.WriteByte('"') } // AppendStringArray appends an array of string literals to buf. func AppendStringArray(buf *bytes.Buffer, a ...string) { buf.WriteByte('[') for i, s := range a { if i > 0 { buf.WriteByte(',') } AppendString(buf, s) } buf.WriteByte(']') } // AppendFloat appends a numeric literal representing the value to buf. func AppendFloat(buf *bytes.Buffer, x float64) error { var scratch [64]byte if math.IsInf(x, 0) || math.IsNaN(x) { return &json.UnsupportedValueError{ Value: reflect.ValueOf(x), Str: strconv.FormatFloat(x, 'g', -1, 64), } } buf.Write(strconv.AppendFloat(scratch[:0], x, 'g', -1, 64)) return nil } // AppendFloat32 appends a numeric literal representingthe value to buf. func AppendFloat32(buf *bytes.Buffer, x float32) error { var scratch [64]byte x64 := float64(x) if math.IsInf(x64, 0) || math.IsNaN(x64) { return &json.UnsupportedValueError{ Value: reflect.ValueOf(x64), Str: strconv.FormatFloat(x64, 'g', -1, 32), } } buf.Write(strconv.AppendFloat(scratch[:0], x64, 'g', -1, 32)) return nil } // AppendFloatArray appends an array of numeric literals to buf. func AppendFloatArray(buf *bytes.Buffer, a ...float64) error { buf.WriteByte('[') for i, x := range a { if i > 0 { buf.WriteByte(',') } if err := AppendFloat(buf, x); err != nil { return err } } buf.WriteByte(']') return nil } // AppendInt appends a numeric literal representing the value to buf. func AppendInt(buf *bytes.Buffer, x int64) { var scratch [64]byte buf.Write(strconv.AppendInt(scratch[:0], x, 10)) } // AppendIntArray appends an array of numeric literals to buf. func AppendIntArray(buf *bytes.Buffer, a ...int64) { var scratch [64]byte buf.WriteByte('[') for i, x := range a { if i > 0 { buf.WriteByte(',') } buf.Write(strconv.AppendInt(scratch[:0], x, 10)) } buf.WriteByte(']') } // AppendUint appends a numeric literal representing the value to buf. func AppendUint(buf *bytes.Buffer, x uint64) { var scratch [64]byte buf.Write(strconv.AppendUint(scratch[:0], x, 10)) } // AppendUintArray appends an array of numeric literals to buf. func AppendUintArray(buf *bytes.Buffer, a ...uint64) { var scratch [64]byte buf.WriteByte('[') for i, x := range a { if i > 0 { buf.WriteByte(',') } buf.Write(strconv.AppendUint(scratch[:0], x, 10)) } buf.WriteByte(']') } go-agent-3.42.0/v3/internal/jsonx/encode_test.go000066400000000000000000000134731510742411500214410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package jsonx import ( "bytes" "fmt" "math" "testing" ) func TestAppendFloat(t *testing.T) { buf := &bytes.Buffer{} err := AppendFloat(buf, math.NaN()) if err == nil { t.Error("AppendFloat(NaN) should return an error") } err = AppendFloat(buf, math.Inf(1)) if err == nil { t.Error("AppendFloat(+Inf) should return an error") } err = AppendFloat(buf, math.Inf(-1)) if err == nil { t.Error("AppendFloat(-Inf) should return an error") } } func TestAppendFloat32(t *testing.T) { buf := &bytes.Buffer{} err := AppendFloat32(buf, float32(math.NaN())) if err == nil { t.Error("AppendFloat(NaN) should return an error") } err = AppendFloat32(buf, float32(math.Inf(1))) if err == nil { t.Error("AppendFloat(+Inf) should return an error") } err = AppendFloat32(buf, float32(math.Inf(-1))) if err == nil { t.Error("AppendFloat(-Inf) should return an error") } err = AppendFloat32(buf, float32(12.5)) if err != nil { t.Error("AppendFloat(12.5) should not return an error") } } func TestAppendFloats(t *testing.T) { buf := &bytes.Buffer{} AppendFloatArray(buf) if want, got := "[]", buf.String(); want != got { t.Errorf("AppendFloatArray(buf)=%q want=%q", got, want) } buf.Reset() AppendFloatArray(buf, 3.14) if want, got := "[3.14]", buf.String(); want != got { t.Errorf("AppendFloatArray(buf)=%q want=%q", got, want) } buf.Reset() AppendFloatArray(buf, 1, 2) if want, got := "[1,2]", buf.String(); want != got { t.Errorf("AppendFloatArray(buf)=%q want=%q", got, want) } } func TestAppendInt(t *testing.T) { buf := &bytes.Buffer{} AppendInt(buf, 42) if got := buf.String(); got != "42" { t.Errorf("AppendUint(42) = %#q want %#q", got, "42") } buf.Reset() AppendInt(buf, -42) if got := buf.String(); got != "-42" { t.Errorf("AppendUint(-42) = %#q want %#q", got, "-42") } } func TestAppendIntArray(t *testing.T) { buf := &bytes.Buffer{} AppendIntArray(buf) if want, got := "[]", buf.String(); want != got { t.Errorf("AppendIntArray(buf)=%q want=%q", got, want) } buf.Reset() AppendIntArray(buf, 42) if want, got := "[42]", buf.String(); want != got { t.Errorf("AppendIntArray(buf)=%q want=%q", got, want) } buf.Reset() AppendIntArray(buf, 1, -2) if want, got := "[1,-2]", buf.String(); want != got { t.Errorf("AppendIntArray(buf)=%q want=%q", got, want) } buf.Reset() AppendIntArray(buf, 1, -2, 0) if want, got := "[1,-2,0]", buf.String(); want != got { t.Errorf("AppendIntArray(buf)=%q want=%q", got, want) } } func TestAppendUint(t *testing.T) { buf := &bytes.Buffer{} AppendUint(buf, 42) if got := buf.String(); got != "42" { t.Errorf("AppendUint(42) = %#q want %#q", got, "42") } } func TestAppendUintArray(t *testing.T) { buf := &bytes.Buffer{} AppendUintArray(buf) if want, got := "[]", buf.String(); want != got { t.Errorf("AppendUintArray(buf)=%q want=%q", got, want) } buf.Reset() AppendUintArray(buf, 42) if want, got := "[42]", buf.String(); want != got { t.Errorf("AppendUintArray(buf)=%q want=%q", got, want) } buf.Reset() AppendUintArray(buf, 1, 2) if want, got := "[1,2]", buf.String(); want != got { t.Errorf("AppendUintArray(buf)=%q want=%q", got, want) } buf.Reset() AppendUintArray(buf, 1, 2, 3) if want, got := "[1,2,3]", buf.String(); want != got { t.Errorf("AppendUintArray(buf)=%q want=%q", got, want) } } var encodeStringTests = []struct { in string out string }{ {"\x00", `"\u0000"`}, {"\x01", `"\u0001"`}, {"\x02", `"\u0002"`}, {"\x03", `"\u0003"`}, {"\x04", `"\u0004"`}, {"\x05", `"\u0005"`}, {"\x06", `"\u0006"`}, {"\x07", `"\u0007"`}, {"\x08", `"\u0008"`}, {"\x09", `"\t"`}, {"\x0a", `"\n"`}, {"\x0b", `"\u000b"`}, {"\x0c", `"\u000c"`}, {"\x0d", `"\r"`}, {"\x0e", `"\u000e"`}, {"\x0f", `"\u000f"`}, {"\x10", `"\u0010"`}, {"\x11", `"\u0011"`}, {"\x12", `"\u0012"`}, {"\x13", `"\u0013"`}, {"\x14", `"\u0014"`}, {"\x15", `"\u0015"`}, {"\x16", `"\u0016"`}, {"\x17", `"\u0017"`}, {"\x18", `"\u0018"`}, {"\x19", `"\u0019"`}, {"\x1a", `"\u001a"`}, {"\x1b", `"\u001b"`}, {"\x1c", `"\u001c"`}, {"\x1d", `"\u001d"`}, {"\x1e", `"\u001e"`}, {"\x1f", `"\u001f"`}, {"\\", `"\\"`}, {`"`, `"\""`}, {"the\u2028quick\t\nbrown\u2029fox", `"the\u2028quick\t\nbrown\u2029fox"`}, //extra edge cases {string([]byte{237, 159, 193}), `"\ufffd\ufffd\ufffd"`}, // invalid utf8 {string([]byte{55, 237, 159, 193, 55}), `"7\ufffd\ufffd\ufffd7"`}, // invalid utf8 surrounded by valid utf8 {`abcdefghijklmnopqrstuvwxyz1234567890`, `"abcdefghijklmnopqrstuvwxyz1234567890"`}, // alphanumeric {"'", `"'"`}, {``, `""`}, {`\`, `"\\"`}, {fmt.Sprintf("%c", rune(65533)), fmt.Sprintf("\"%c\"", rune(65533))}, // invalid rune utf8 symbol (valid utf8) } func TestAppendString(t *testing.T) { buf := &bytes.Buffer{} for _, tt := range encodeStringTests { buf.Reset() AppendString(buf, tt.in) if got := buf.String(); got != tt.out { t.Errorf("AppendString(%q) = %#q, want %#q", tt.in, got, tt.out) } } } func TestAppendStringArray(t *testing.T) { buf := &bytes.Buffer{} var encodeStringArrayTests = []struct { in []string out string }{ { in: []string{ "hi", "foo", }, out: `["hi","foo"]`, }, { in: []string{ "foo", }, out: `["foo"]`, }, { in: []string{}, out: `[]`, }, } for _, tt := range encodeStringArrayTests { buf.Reset() AppendStringArray(buf, tt.in...) if got := buf.String(); got != tt.out { t.Errorf("AppendString(%q) = %#q, want %#q", tt.in, got, tt.out) } } } func BenchmarkAppendString(b *testing.B) { buf := &bytes.Buffer{} for i := 0; i < b.N; i++ { AppendString(buf, "s") } } func BenchmarkAppendString10(b *testing.B) { buf := &bytes.Buffer{} for i := 0; i < b.N; i++ { AppendString(buf, "qwertyuiop") } } go-agent-3.42.0/v3/internal/limits.go000066400000000000000000000027311510742411500173000ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal const ( // app behavior // DefaultConfigurableEventHarvestMs is the period for custom, error, // and transaction events if the connect response's // "event_harvest_config.report_period_ms" is missing or invalid. DefaultConfigurableEventHarvestMs = 60 * 1000 // MaxPayloadSizeInBytes specifies the maximum payload size in bytes that // should be sent to any endpoint MaxPayloadSizeInBytes = 1000 * 1000 // MaxCustomEvents is the maximum number of Custom Events that can be captured // per 60-second harvest cycle MaxCustomEvents = 100 * 1000 // DefaultCustomEvents is the default number of Custom Events that can be captured // per 60-second harvest cycle DefaultCustomEvents = 30 * 1000 // MaxLogEvents is the maximum number of Log Events that can be captured per // 60-second harvest cycle MaxLogEvents = 10 * 1000 // MaxTxnEvents is the maximum number of Transaction Events that can be captured // per 60-second harvest cycle MaxTxnEvents = 10 * 1000 // MaxErrorEvents is the maximum number of Error Events that can be captured // per 60-second harvest cycle MaxErrorEvents = 100 // MaxSpanEvents is the maximum number of Spans Events that can be captured // per 60-second harvest cycle MaxSpanEvents = 2000 // DefaultSpanEvents in the default number of Span Events that can be captured // per 60-second harvest cycle DefaultSpanEvents = 2000 ) go-agent-3.42.0/v3/internal/logcontext/000077500000000000000000000000001510742411500176335ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/logcontext/decoratorTesting.go000066400000000000000000000052571510742411500235130ustar00rootroot00000000000000package logcontext import ( "bytes" "strings" "testing" ) // DecorationExpect defines the expected values a log decorated by a logcontext v2 decorator should have type DecorationExpect struct { DecorationDisabled bool EntityGUID string EntityName string Hostname string TraceID string SpanID string // decorator errors will result in an undecorated log message being printed // and an error message also being printed in a separate line DecoratorError error } // metadata indexes const ( entityguid = 1 hostname = 2 traceid = 3 spanid = 4 ) func entityname(vals []string) string { if len(vals) < 2 { return "" } return vals[len(vals)-2] } // ValidateDecoratedOutput is a testing tool that validates whether a bytes buffer decorated by a // logcontext v2 decorator contains the values we expect it to. func ValidateDecoratedOutput(t *testing.T, out *bytes.Buffer, expect *DecorationExpect) { actual := out.String() if expect.DecorationDisabled { if strings.Contains(actual, "NR-LINKING") { t.Fatal("log decoration was expected to be disabled, but were decorated anyway") } else { return } } if expect.DecoratorError != nil { if strings.Contains(actual, "NR-LINKING") { t.Fatal("logs should not be decorated when a decorator error occurs") } msg := expect.DecoratorError.Error() if !strings.Contains(actual, msg) { t.Fatalf("an error message debug log was expected, \"%s\", but was not found: %s", msg, actual) } else { return } } split := strings.Split(actual, "NR-LINKING") if len(split) != 2 { t.Fatalf("expected log decoration, but NR-LINKING data was missing: %s", actual) } linkingData := strings.Split(split[1], "|") if len(linkingData) < 5 { t.Errorf("linking data is missing required fields: %s", split[1]) } if linkingData[entityguid] != expect.EntityGUID { t.Errorf("incorrect entity GUID; expect: %s actual: %s", expect.EntityGUID, linkingData[entityguid]) } if linkingData[hostname] != expect.Hostname { t.Errorf("incorrect hostname; expect: %s actual: %s", expect.Hostname, linkingData[hostname]) } if entityname(linkingData) != expect.EntityName { t.Errorf("incorrect entity name; expect: %s actual: %s", expect.EntityName, entityname(linkingData)) } if expect.TraceID != "" && expect.SpanID != "" { if len(linkingData) < 7 { t.Errorf("transaction metadata is missing from linking data: %s", split[1]) } if linkingData[traceid] != expect.TraceID { t.Errorf("incorrect traceID; expect: %s actual: %s", expect.TraceID, linkingData[traceid]) } if linkingData[spanid] != expect.SpanID { t.Errorf("incorrect hostname; expect: %s actual: %s", expect.SpanID, linkingData[spanid]) } } } go-agent-3.42.0/v3/internal/logcontext/publicConstants.go000066400000000000000000000016151510742411500233400ustar00rootroot00000000000000package logcontext // Exported Constants for log decorators const ( // LogSeverityFieldName is the name of the log level field in New Relic logging JSON LogSeverityFieldName = "level" // LogMessageFieldName is the name of the log message field in New Relic logging JSON LogMessageFieldName = "message" // LogTimestampFieldName is the name of the timestamp field in New Relic logging JSON LogTimestampFieldName = "timestamp" // LogSpanIDFieldName is the name of the span ID field in the New Relic logging JSON LogSpanIDFieldName = "span.id" // LogTraceIDFieldName is the name of the trace ID field in the New Relic logging JSON LogTraceIDFieldName = "trace.id" // LogSeverityUnknown is the value the log severity should be set to if no log severity is known LogSeverityUnknown = "UNKNOWN" // number of bytes expected to be needed for the average log message AverageLogSizeEstimate = 400 ) go-agent-3.42.0/v3/internal/logger/000077500000000000000000000000001510742411500167245ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/logger/logger.go000066400000000000000000000052721510742411500205400ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package logger import ( "encoding/json" "fmt" "io" "log" "os" "regexp" ) // Logger matches newrelic.Logger to allow implementations to be passed to // internal packages. type Logger interface { Error(msg string, context map[string]interface{}) Warn(msg string, context map[string]interface{}) Info(msg string, context map[string]interface{}) Debug(msg string, context map[string]interface{}) DebugEnabled() bool } // ShimLogger implements Logger and does nothing. type ShimLogger struct { // IsDebugEnabled is useful as it allows DebugEnabled code paths to be // tested. IsDebugEnabled bool } // Error allows ShimLogger to implement Logger. func (s ShimLogger) Error(string, map[string]interface{}) {} // Warn allows ShimLogger to implement Logger. func (s ShimLogger) Warn(string, map[string]interface{}) {} // Info allows ShimLogger to implement Logger. func (s ShimLogger) Info(string, map[string]interface{}) {} // Debug allows ShimLogger to implement Logger. func (s ShimLogger) Debug(string, map[string]interface{}) {} // DebugEnabled allows ShimLogger to implement Logger. func (s ShimLogger) DebugEnabled() bool { return s.IsDebugEnabled } type logFile struct { l *log.Logger doDebug bool } // New creates a basic Logger. func New(w io.Writer, doDebug bool) Logger { return &logFile{ l: log.New(w, logPid, logFlags), doDebug: doDebug, } } const logFlags = log.Ldate | log.Ltime | log.Lmicroseconds var ( logPid = fmt.Sprintf("(%d) ", os.Getpid()) ) func (f *logFile) fire(level, msg string, ctx map[string]interface{}) { js, err := json.Marshal(struct { Level string `json:"level"` Event string `json:"msg"` Context map[string]interface{} `json:"context"` }{ level, msg, ctx, }) if err == nil { // scrub license keys from any portion of the log message re := regexp.MustCompile(`license_key=[a-fA-F0-9.]+`) sanitized := re.ReplaceAllLiteralString(string(js), "license_key=[redacted]") f.l.Print(sanitized) } else { f.l.Printf("unable to marshal log entry") // error value removed from message to avoid possibility of sensitive // content being leaked that way } } func (f *logFile) Error(msg string, ctx map[string]interface{}) { f.fire("error", msg, ctx) } func (f *logFile) Warn(msg string, ctx map[string]interface{}) { f.fire("warn", msg, ctx) } func (f *logFile) Info(msg string, ctx map[string]interface{}) { f.fire("info", msg, ctx) } func (f *logFile) Debug(msg string, ctx map[string]interface{}) { if f.doDebug { f.fire("debug", msg, ctx) } } func (f *logFile) DebugEnabled() bool { return f.doDebug } go-agent-3.42.0/v3/internal/logger/logger_test.go000066400000000000000000000032501510742411500215710ustar00rootroot00000000000000package logger import ( "bytes" "encoding/json" "strings" "testing" ) func TestShimLogger(t *testing.T) { logger := ShimLogger{IsDebugEnabled: true} //do nothing m := map[string]interface{}{"key1": "val1", "key2": "val2"} logger.Error("Do nothing", m) logger.Warn("Do nothing", m) logger.Info("Do nothing", m) logger.Debug("Do nothing", m) enabled := logger.DebugEnabled() if !enabled { t.Error("Debug logging is not enabled") } } func TestBasicLogger(t *testing.T) { b := &bytes.Buffer{} logger := New(b, true) m := map[string]interface{}{"key1": "val1", "key2": "val2"} logger.Error("error message", m) logger.Warn("warn message", m) logger.Info("info message", m) logger.Debug("debug message", m) enabled := logger.DebugEnabled() if !enabled { t.Error("Debug logging is not enabled") } var expected []string expected = append(expected, "{\"level\":\"error\",\"msg\":\"error message\",\"context\":{\"key1\":\"val1\",\"key2\":\"val2\"}}", "{\"level\":\"warn\",\"msg\":\"warn message\",\"context\":{\"key1\":\"val1\",\"key2\":\"val2\"}}", "{\"level\":\"info\",\"msg\":\"info message\",\"context\":{\"key1\":\"val1\",\"key2\":\"val2\"}}", "{\"level\":\"debug\",\"msg\":\"debug message\",\"context\":{\"key1\":\"val1\",\"key2\":\"val2\"}}") var jsonMap map[string]interface{} s := strings.Split(b.String(), "\n") s = s[:len(s)-1] for i, v := range s { jsonStr := v[strings.Index(v, "{"):] err := json.Unmarshal([]byte(jsonStr), &jsonMap) if err != nil { t.Errorf("Error %v unmarshaling JSON:", err) } if jsonStr != expected[i] { t.Errorf("JSON string does not match expected:\n\tExpected: %v\n\tActual: %v", expected[i], jsonStr) } } } go-agent-3.42.0/v3/internal/message_metric_key.go000066400000000000000000000022771510742411500216430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal // MessageMetricKey is the key to use for message segments. type MessageMetricKey struct { Library string DestinationType string Consumer bool DestinationName string DestinationTemp bool } // Name returns the metric name value for this MessageMetricKey to be used for // scoped and unscoped metrics. // // Producers // MessageBroker/{Library}/{Destination Type}/{Action}/Named/{Destination Name} // MessageBroker/{Library}/{Destination Type}/{Action}/Temp // // Consumers // OtherTransaction/Message/{Library}/{DestinationType}/Named/{Destination Name} // OtherTransaction/Message/{Library}/{DestinationType}/Temp func (key MessageMetricKey) Name() string { var destination string if key.DestinationTemp { destination = "Temp" } else if key.DestinationName == "" { destination = "Named/Unknown" } else { destination = "Named/" + key.DestinationName } if key.Consumer { return "Message/" + key.Library + "/" + key.DestinationType + "/" + destination } return "MessageBroker/" + key.Library + "/" + key.DestinationType + "/Produce/" + destination } go-agent-3.42.0/v3/internal/message_metric_key_test.go000066400000000000000000000056721510742411500227040ustar00rootroot00000000000000package internal import "testing" func TestNameWithConsumerMessageMetricKey(t *testing.T) { consumer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: true, DestinationName: "DestinationName", DestinationTemp: false, } expect := "Message/" + consumer.Library + "/" + consumer.DestinationType + "/" + "Named" + "/" + consumer.DestinationName actual := MessageMetricKey.Name(consumer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } func TestNameWithConsumerMessageMetricKeyWithDestinationTemp(t *testing.T) { consumer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: true, DestinationName: "DestinationName", DestinationTemp: true, } expect := "Message/" + consumer.Library + "/" + consumer.DestinationType + "/" + "Temp" actual := MessageMetricKey.Name(consumer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } func TestNameWithConsumerMessageMetricKeyWithEmptyDestinationName(t *testing.T) { consumer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: true, DestinationName: "", DestinationTemp: false, } expect := "Message/" + consumer.Library + "/" + consumer.DestinationType + "/" + "Named/Unknown" actual := MessageMetricKey.Name(consumer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } func TestNameWithProducerMessageMetricKey(t *testing.T) { producer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: false, DestinationName: "DestinationName", DestinationTemp: true, } expect := "MessageBroker/" + producer.Library + "/" + producer.DestinationType + "/" + "Produce" + "/" + "Temp" actual := MessageMetricKey.Name(producer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } func TestNameWithProducerMessageMetricKeyWithDestinationName(t *testing.T) { producer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: false, DestinationName: "DestinationName", DestinationTemp: false, } expect := "MessageBroker/" + producer.Library + "/" + producer.DestinationType + "/" + "Produce" + "/" + "Named" + "/" + producer.DestinationName actual := MessageMetricKey.Name(producer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } func TestNameWithProducerMessageMetricKeyWithEmptyDestinationName(t *testing.T) { producer := MessageMetricKey{ Library: "hello", DestinationType: "DestinationType", Consumer: false, DestinationName: "", DestinationTemp: false, } expect := "MessageBroker/" + producer.Library + "/" + producer.DestinationType + "/" + "Produce" + "/" + "Named" + "/" + "Unknown" actual := MessageMetricKey.Name(producer) if expect != actual { t.Errorf("got %v, want %v", actual, expect) } } go-agent-3.42.0/v3/internal/metric_rules.go000066400000000000000000000107771510742411500205050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "regexp" "sort" "strings" ) type ruleResult int const ( ruleMatched ruleResult = iota ruleUnmatched ruleIgnore ) type metricRule struct { // 'Ignore' indicates if the entire transaction should be discarded if // there is a match. This field is only used by "url_rules" and // "transaction_name_rules", not "metric_name_rules". Ignore bool `json:"ignore"` EachSegment bool `json:"each_segment"` ReplaceAll bool `json:"replace_all"` Terminate bool `json:"terminate_chain"` Order int `json:"eval_order"` OriginalReplacement string `json:"replacement"` RawExpr string `json:"match_expression"` // Go's regexp backreferences use '${1}' instead of the Perlish '\1', so // we transform the replacement string into the Go syntax and store it // here. TransformedReplacement string re *regexp.Regexp } // MetricRules is a collection of metric rules. type MetricRules []*metricRule // Go's regexp backreferences use `${1}` instead of the Perlish `\1`, so we must // transform the replacement string. This is non-trivial: `\1` is a // backreference but `\\1` is not. Rather than count the number of back slashes // preceding the digit, we simply skip rules with tricky replacements. var ( transformReplacementAmbiguous = regexp.MustCompile(`\\\\([0-9]+)`) transformReplacementRegex = regexp.MustCompile(`\\([0-9]+)`) transformReplacementReplacement = "$${${1}}" ) // UnmarshalJSON unmarshals rules from connect reply JSON. func (rules *MetricRules) UnmarshalJSON(data []byte) (err error) { var raw []*metricRule if err := json.Unmarshal(data, &raw); nil != err { return err } valid := make(MetricRules, 0, len(raw)) for _, r := range raw { re, err := regexp.Compile("(?i)" + r.RawExpr) if err != nil { // TODO // Warn("unable to compile rule", { // "match_expression": r.RawExpr, // "error": err.Error(), // }) continue } if transformReplacementAmbiguous.MatchString(r.OriginalReplacement) { // TODO // Warn("unable to transform replacement", { // "match_expression": r.RawExpr, // "replacement": r.OriginalReplacement, // }) continue } r.re = re r.TransformedReplacement = transformReplacementRegex.ReplaceAllString(r.OriginalReplacement, transformReplacementReplacement) valid = append(valid, r) } sort.Sort(valid) *rules = valid return nil } // Len returns the number of rules. func (rules MetricRules) Len() int { return len(rules) } // Rules should be applied in increasing order func (rules MetricRules) Less(i, j int) bool { return rules[i].Order < rules[j].Order } // Swap is used for sorting. func (rules MetricRules) Swap(i, j int) { rules[i], rules[j] = rules[j], rules[i] } func replaceFirst(re *regexp.Regexp, s string, replacement string) (ruleResult, string) { // Note that ReplaceAllStringFunc cannot be used here since it does // not replace $1 placeholders. loc := re.FindStringIndex(s) if nil == loc { return ruleUnmatched, s } firstMatch := s[loc[0]:loc[1]] firstMatchReplaced := re.ReplaceAllString(firstMatch, replacement) return ruleMatched, s[0:loc[0]] + firstMatchReplaced + s[loc[1]:] } func (r *metricRule) apply(s string) (ruleResult, string) { // Rules are strange, and there is no spec. // This code attempts to duplicate the logic of the PHP agent. // Ambiguity abounds. if r.Ignore { if r.re.MatchString(s) { return ruleIgnore, "" } return ruleUnmatched, s } if r.ReplaceAll { if r.re.MatchString(s) { return ruleMatched, r.re.ReplaceAllString(s, r.TransformedReplacement) } return ruleUnmatched, s } else if r.EachSegment { segments := strings.Split(s, "/") applied := make([]string, len(segments)) result := ruleUnmatched for i, segment := range segments { var segmentMatched ruleResult segmentMatched, applied[i] = replaceFirst(r.re, segment, r.TransformedReplacement) if segmentMatched == ruleMatched { result = ruleMatched } } return result, strings.Join(applied, "/") } else { return replaceFirst(r.re, s, r.TransformedReplacement) } } // Apply applies the rules. func (rules MetricRules) Apply(input string) string { var res ruleResult s := input for _, rule := range rules { res, s = rule.apply(s) if ruleIgnore == res { return "" } if (ruleMatched == res) && rule.Terminate { break } } return s } go-agent-3.42.0/v3/internal/metric_rules_test.go000066400000000000000000000041011510742411500215240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestMetricRules(t *testing.T) { var tcs []struct { Testname string `json:"testname"` Rules MetricRules `json:"rules"` Tests []struct { Input string `json:"input"` Expected string `json:"expected"` } `json:"tests"` } err := crossagent.ReadJSON("rules.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { // This test relies upon Perl-specific regex syntax (negative // lookahead assertions) which are not implemented in Go's // regexp package. We believe these types of rules are // exceedingly rare in practice, so we're skipping // implementation of this exotic syntax for now. if tc.Testname == "saxon's test" { continue } for _, x := range tc.Tests { out := tc.Rules.Apply(x.Input) if out != x.Expected { t.Fatal(tc.Testname, x.Input, out, x.Expected) } } } } func TestMetricRuleWithNegativeLookaheadAssertion(t *testing.T) { js := `[{ "match_expression":"^(?!account|application).*", "replacement":"*", "ignore":false, "eval_order":0, "each_segment":true }]` var rules MetricRules err := json.Unmarshal([]byte(js), &rules) if nil != err { t.Fatal(err) } if 0 != rules.Len() { t.Fatal(rules) } } func TestNilApplyRules(t *testing.T) { var rules MetricRules input := "hello" out := rules.Apply(input) if input != out { t.Fatal(input, out) } } func TestAmbiguousReplacement(t *testing.T) { js := `[{ "match_expression":"(.*)/[^/]*.(bmp|css|gif|ico|jpg|jpeg|js|png)", "replacement":"\\\\1/*.\\2", "ignore":false, "eval_order":0 }]` var rules MetricRules err := json.Unmarshal([]byte(js), &rules) if nil != err { t.Fatal(err) } if 0 != rules.Len() { t.Fatal(rules) } } func TestBadMetricRulesJSON(t *testing.T) { js := `{}` var rules MetricRules err := json.Unmarshal([]byte(js), &rules) if nil == err { t.Fatal("missing bad json error") } } go-agent-3.42.0/v3/internal/security_policies.go000066400000000000000000000062231510742411500215350ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "fmt" "reflect" ) // Security policies documentation: // https://source.datanerd.us/agents/agent-specs/blob/master/Language-Agent-Security-Policies.md // SecurityPolicies contains the security policies. type SecurityPolicies struct { RecordSQL securityPolicy `json:"record_sql"` AttributesInclude securityPolicy `json:"attributes_include"` AllowRawExceptionMessages securityPolicy `json:"allow_raw_exception_messages"` CustomEvents securityPolicy `json:"custom_events"` CustomParameters securityPolicy `json:"custom_parameters"` } // PointerIfPopulated returns a reference to the security policies if they have // been populated from JSON. func (sp *SecurityPolicies) PointerIfPopulated() *SecurityPolicies { emptyPolicies := SecurityPolicies{} if nil != sp && *sp != emptyPolicies { return sp } return nil } type securityPolicy struct { EnabledVal *bool `json:"enabled"` } func (p *securityPolicy) Enabled() bool { return nil == p.EnabledVal || *p.EnabledVal } func (p *securityPolicy) SetEnabled(enabled bool) { p.EnabledVal = &enabled } func (p *securityPolicy) IsSet() bool { return nil != p.EnabledVal } type policyer interface { SetEnabled(bool) IsSet() bool } // UnmarshalJSON decodes security policies sent from the preconnect endpoint. func (sp *SecurityPolicies) UnmarshalJSON(data []byte) (er error) { defer func() { // Zero out all fields if there is an error to ensure that the // populated check works. if er != nil { *sp = SecurityPolicies{} } }() var raw map[string]struct { Enabled bool `json:"enabled"` Required bool `json:"required"` } err := json.Unmarshal(data, &raw) if err != nil { return fmt.Errorf("unable to unmarshal security policies: %v", err) } knownPolicies := make(map[string]policyer) spv := reflect.ValueOf(sp).Elem() for i := 0; i < spv.NumField(); i++ { fieldAddress := spv.Field(i).Addr() field := fieldAddress.Interface().(policyer) name := spv.Type().Field(i).Tag.Get("json") knownPolicies[name] = field } for name, policy := range raw { p, ok := knownPolicies[name] if !ok { if policy.Required { return errUnknownRequiredPolicy{name: name} } } else { p.SetEnabled(policy.Enabled) } } for name, policy := range knownPolicies { if !policy.IsSet() { return errUnsetPolicy{name: name} } } return nil } type errUnknownRequiredPolicy struct{ name string } func (err errUnknownRequiredPolicy) Error() string { return fmt.Sprintf("policy '%s' is unrecognized, please check for a newer agent version or contact support", err.name) } type errUnsetPolicy struct{ name string } func (err errUnsetPolicy) Error() string { return fmt.Sprintf("policy '%s' not received, please contact support", err.name) } // IsDisconnectSecurityPolicyError indicates if the error is disconnect worthy. func IsDisconnectSecurityPolicyError(e error) bool { if _, ok := e.(errUnknownRequiredPolicy); ok { return true } if _, ok := e.(errUnsetPolicy); ok { return true } return false } go-agent-3.42.0/v3/internal/security_policies_test.go000066400000000000000000000114131510742411500225710ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "testing" ) func testBool(t *testing.T, name string, expected, got bool) { if expected != got { t.Errorf("%v: expected=%v got=%v", name, expected, got) } } func TestSecurityPoliciesPresent(t *testing.T) { inputJSON := []byte(`{ "record_sql": { "enabled": false, "required": false }, "attributes_include": { "enabled": false, "required": false }, "allow_raw_exception_messages": { "enabled": false, "required": false }, "custom_events": { "enabled": false, "required": false }, "custom_parameters": { "enabled": false, "required": false }, "custom_instrumentation_editor": { "enabled": false, "required": false }, "message_parameters": { "enabled": false, "required": false }, "job_arguments": { "enabled": false, "required": false } }`) var policies SecurityPolicies err := json.Unmarshal(inputJSON, &policies) if nil != err { t.Fatal(err) } connectJSON, err := json.Marshal(policies) if nil != err { t.Fatal(err) } expectJSON := CompactJSONString(`{ "record_sql": { "enabled": false }, "attributes_include": { "enabled": false }, "allow_raw_exception_messages": { "enabled": false }, "custom_events": { "enabled": false }, "custom_parameters": { "enabled": false } }`) if string(connectJSON) != expectJSON { t.Error(string(connectJSON), expectJSON) } testBool(t, "PointerIfPopulated", true, nil != policies.PointerIfPopulated()) testBool(t, "RecordSQLEnabled", false, policies.RecordSQL.Enabled()) testBool(t, "AttributesIncludeEnabled", false, policies.AttributesInclude.Enabled()) testBool(t, "AllowRawExceptionMessages", false, policies.AllowRawExceptionMessages.Enabled()) testBool(t, "CustomEventsEnabled", false, policies.CustomEvents.Enabled()) testBool(t, "CustomParametersEnabled", false, policies.CustomParameters.Enabled()) } func TestNilSecurityPolicies(t *testing.T) { var policies SecurityPolicies testBool(t, "PointerIfPopulated", false, nil != policies.PointerIfPopulated()) testBool(t, "RecordSQLEnabled", true, policies.RecordSQL.Enabled()) testBool(t, "AttributesIncludeEnabled", true, policies.AttributesInclude.Enabled()) testBool(t, "AllowRawExceptionMessages", true, policies.AllowRawExceptionMessages.Enabled()) testBool(t, "CustomEventsEnabled", true, policies.CustomEvents.Enabled()) testBool(t, "CustomParametersEnabled", true, policies.CustomParameters.Enabled()) } func TestUnknownRequiredPolicy(t *testing.T) { inputJSON := []byte(`{ "record_sql": { "enabled": false, "required": false }, "attributes_include": { "enabled": false, "required": false }, "allow_raw_exception_messages": { "enabled": false, "required": false }, "custom_events": { "enabled": false, "required": false }, "custom_parameters": { "enabled": false, "required": false }, "custom_instrumentation_editor": { "enabled": false, "required": false }, "message_parameters": { "enabled": false, "required": false }, "job_arguments": { "enabled": false, "required": false }, "unknown_policy": { "enabled": false, "required": true } }`) var policies SecurityPolicies err := json.Unmarshal(inputJSON, &policies) if nil == err { t.Fatal(err) } testBool(t, "PointerIfPopulated", false, nil != policies.PointerIfPopulated()) testBool(t, "unknown required policy should be disconnect", true, IsDisconnectSecurityPolicyError(err)) } func TestSecurityPolicyMissing(t *testing.T) { inputJSON := []byte(`{ "record_sql": { "enabled": false, "required": false }, "attributes_include": { "enabled": false, "required": false }, "allow_raw_exception_messages": { "enabled": false, "required": false }, "custom_events": { "enabled": false, "required": false }, "request_parameters": { "enabled": false, "required": false } }`) var policies SecurityPolicies err := json.Unmarshal(inputJSON, &policies) _, ok := err.(errUnsetPolicy) if !ok { t.Fatal(err) } testBool(t, "PointerIfPopulated", false, nil != policies.PointerIfPopulated()) testBool(t, "missing policy should be disconnect", true, IsDisconnectSecurityPolicyError(err)) } func TestMalformedPolicies(t *testing.T) { inputJSON := []byte(`{`) var policies SecurityPolicies err := json.Unmarshal(inputJSON, &policies) if nil == err { t.Fatal(err) } testBool(t, "malformed policies should not be disconnect", false, IsDisconnectSecurityPolicyError(err)) } go-agent-3.42.0/v3/internal/segment_terms.go000066400000000000000000000055051510742411500206550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal // https://newrelic.atlassian.net/wiki/display/eng/Language+agent+transaction+segment+terms+rules import ( "encoding/json" "strings" ) const ( placeholder = "*" separator = "/" ) type segmentRule struct { Prefix string `json:"prefix"` Terms []string `json:"terms"` TermsMap map[string]struct{} } // segmentRules is keyed by each segmentRule's Prefix field with any trailing // slash removed. type segmentRules map[string]*segmentRule func buildTermsMap(terms []string) map[string]struct{} { m := make(map[string]struct{}, len(terms)) for _, t := range terms { m[t] = struct{}{} } return m } func (rules *segmentRules) UnmarshalJSON(b []byte) error { var raw []*segmentRule if err := json.Unmarshal(b, &raw); nil != err { return err } rs := make(map[string]*segmentRule) for _, rule := range raw { prefix := strings.TrimSuffix(rule.Prefix, "/") if len(strings.Split(prefix, "/")) != 2 { // TODO // Warn("invalid segment term rule prefix", // {"prefix": rule.Prefix}) continue } if nil == rule.Terms { // TODO // Warn("segment term rule has missing terms", // {"prefix": rule.Prefix}) continue } rule.TermsMap = buildTermsMap(rule.Terms) rs[prefix] = rule } *rules = rs return nil } func (rule *segmentRule) apply(name string) string { if !strings.HasPrefix(name, rule.Prefix) { return name } s := strings.TrimPrefix(name, rule.Prefix) leadingSlash := "" if strings.HasPrefix(s, separator) { leadingSlash = separator s = strings.TrimPrefix(s, separator) } if "" != s { segments := strings.Split(s, separator) for i, segment := range segments { _, allowed := rule.TermsMap[segment] if allowed { segments[i] = segment } else { segments[i] = placeholder } } segments = collapsePlaceholders(segments) s = strings.Join(segments, separator) } return rule.Prefix + leadingSlash + s } func (rules segmentRules) apply(name string) string { if nil == rules { return name } rule, ok := rules[firstTwoSegments(name)] if !ok { return name } return rule.apply(name) } func firstTwoSegments(name string) string { firstSlashIdx := strings.Index(name, separator) if firstSlashIdx == -1 { return name } secondSlashIdx := strings.Index(name[firstSlashIdx+1:], separator) if secondSlashIdx == -1 { return name } return name[0 : firstSlashIdx+secondSlashIdx+1] } func collapsePlaceholders(segments []string) []string { j := 0 prevStar := false for i := 0; i < len(segments); i++ { segment := segments[i] if placeholder == segment { if prevStar { continue } segments[j] = placeholder j++ prevStar = true } else { segments[j] = segment j++ prevStar = false } } return segments[0:j] } go-agent-3.42.0/v3/internal/segment_terms_test.go000066400000000000000000000056401510742411500217140ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "encoding/json" "strings" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestCrossAgentSegmentTerms(t *testing.T) { var tcs []struct { Testname string `json:"testname"` Rules segmentRules `json:"transaction_segment_terms"` Tests []struct { Input string `json:"input"` Expected string `json:"expected"` } `json:"tests"` } err := crossagent.ReadJSON("transaction_segment_terms.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { for _, test := range tc.Tests { out := tc.Rules.apply(test.Input) if out != test.Expected { t.Fatal(tc.Testname, test.Input, out, test.Expected) } } } } func TestSegmentTerms(t *testing.T) { js := `[ { "prefix":"WebTransaction\/Uri", "terms":[ "two", "Users", "willhf", "dev", "php", "one", "alpha", "zap" ] } ]` var rules segmentRules if err := json.Unmarshal([]byte(js), &rules); nil != err { t.Fatal(err) } out := rules.apply("WebTransaction/Uri/pen/two/pencil/dev/paper") if out != "WebTransaction/Uri/*/two/*/dev/*" { t.Fatal(out) } } func TestEmptySegmentTerms(t *testing.T) { var rules segmentRules input := "my/name" out := rules.apply(input) if out != input { t.Error(input, out) } } func BenchmarkSegmentTerms(b *testing.B) { js := `[ { "prefix":"WebTransaction\/Uri", "terms":[ "two", "Users", "willhf", "dev", "php", "one", "alpha", "zap" ] } ]` var rules segmentRules if err := json.Unmarshal([]byte(js), &rules); nil != err { b.Fatal(err) } b.ResetTimer() b.ReportAllocs() input := "WebTransaction/Uri/pen/two/pencil/dev/paper" expected := "WebTransaction/Uri/*/two/*/dev/*" for i := 0; i < b.N; i++ { out := rules.apply(input) if out != expected { b.Fatal(out, expected) } } } func TestCollapsePlaceholders(t *testing.T) { testcases := []struct { input string expect string }{ {input: "", expect: ""}, {input: "/", expect: "/"}, {input: "*", expect: "*"}, {input: "*/*", expect: "*"}, {input: "a/b/c", expect: "a/b/c"}, {input: "*/*/*", expect: "*"}, {input: "a/*/*/*/b", expect: "a/*/b"}, {input: "a/b/*/*/*/", expect: "a/b/*/"}, {input: "a/b/*/*/*", expect: "a/b/*"}, {input: "*/*/a/b/*/*/*", expect: "*/a/b/*"}, {input: "*/*/a/b/*/c/*/*/d/e/*/*/*", expect: "*/a/b/*/c/*/d/e/*"}, {input: "a/*/b", expect: "a/*/b"}, } for _, tc := range testcases { segments := strings.Split(tc.input, "/") segments = collapsePlaceholders(segments) out := strings.Join(segments, "/") if out != tc.expect { t.Error(tc.input, tc.expect, out) } } } go-agent-3.42.0/v3/internal/stacktracetest/000077500000000000000000000000001510742411500204715ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/stacktracetest/stacktracetest.go000066400000000000000000000014021510742411500240410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package stacktracetest helps test stack trace behavior. package stacktracetest // TopStackFrame is a function should will appear in the stacktrace. func TopStackFrame(generateStacktrace func() []byte) []byte { return generateStacktrace() } // CountedCall is a function that allows you to generate a stack trace with this function being called a particular // number of times. The parameter f should be a function that returns a StackTrace (but it is referred to as []uintptr // in order to not create a circular dependency on the internal package) func CountedCall(i int, f func() []uintptr) []uintptr { if i > 0 { return CountedCall(i-1, f) } return f() } go-agent-3.42.0/v3/internal/sysinfo/000077500000000000000000000000001510742411500171375ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/sysinfo/bootid.go000066400000000000000000000023161510742411500207500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "bytes" "fmt" "io/ioutil" "runtime" ) // BootID returns the boot ID of the executing kernel. func BootID() (string, error) { if "linux" != runtime.GOOS { return "", ErrFeatureUnsupported } data, err := ioutil.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil { return "", err } return validateBootID(data) } type invalidBootID string func (e invalidBootID) Error() string { return fmt.Sprintf("Boot id has unrecognized format, id=%q", string(e)) } func isASCIIByte(b byte) bool { return (b >= 0x20 && b <= 0x7f) } func validateBootID(data []byte) (string, error) { // We're going to go for the permissive reading of // https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md: // any ASCII (excluding control characters, because I'm pretty sure that's not // in the spirit of the spec) string will be sent up to and including 128 // bytes in length. trunc := bytes.TrimSpace(data) if len(trunc) > 128 { trunc = trunc[:128] } for _, b := range trunc { if !isASCIIByte(b) { return "", invalidBootID(data) } } return string(trunc), nil } go-agent-3.42.0/v3/internal/sysinfo/docker.go000066400000000000000000000070751510742411500207460ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "bufio" "bytes" "errors" "fmt" "io" "os" "regexp" "runtime" "strings" ) var ( // ErrDockerNotFound is returned if a Docker ID is not found in // /proc/self/cgroup ErrDockerNotFound = errors.New("Docker ID not found") ) // DockerID attempts to detect Docker. func DockerID() (string, error) { if "linux" != runtime.GOOS { return "", ErrFeatureUnsupported } f, err := os.Open("/proc/self/cgroup") if err != nil { return "", err } defer f.Close() id, err := parseDockerID(f) // Attempt mountinfo file lookup if DockerID not found in cgroup file if err == ErrDockerNotFound { f, err := os.Open("/proc/self/mountinfo") if err != nil { return "", err } defer f.Close() return parseDockerIDMountInfo(f) } return id, err } var ( // The DockerID must be a 64-character lowercase hex string // be greedy and match anything 64-characters or longer to spot invalid IDs dockerIDLength = 64 dockerIDRegexRaw = fmt.Sprintf("[0-9a-f]{%d,}", dockerIDLength) dockerIDRegex = regexp.MustCompile(dockerIDRegexRaw) ) func parseDockerIDMountInfo(r io.Reader) (string, error) { // Each Line in the mountinfo file starts with a set of IDs before showing the path file we actually want // 1. Mount ID // 2. Parent ID // 3. Major and minor device numbers // 4. Path to ContainerID scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if strings.Contains(line, "/docker/containers/") { id := dockerIDRegex.FindString(line) if err := validateDockerID(id); err != nil { return "", err } return id, nil } } return "", ErrDockerNotFound } func parseDockerID(r io.Reader) (string, error) { // Each line in the cgroup file consists of three colon delimited fields. // 1. hierarchy ID - we don't care about this // 2. subsystems - comma separated list of cgroup subsystem names // 3. control group - control group to which the process belongs // // Example // 5:cpuacct,cpu,cpuset:/daemons var id string for scanner := bufio.NewScanner(r); scanner.Scan(); { line := scanner.Bytes() cols := bytes.SplitN(line, []byte(":"), 3) if len(cols) < 3 { continue } // We're only interested in the cpu subsystem. if !isCPUCol(cols[1]) { continue } id = dockerIDRegex.FindString(string(cols[2])) if err := validateDockerID(id); err != nil { // We can stop searching at this point, the CPU // subsystem should only occur once, and its cgroup is // not docker or not a format we accept. return "", err } return id, nil } return "", ErrDockerNotFound } func isCPUCol(col []byte) bool { // Sometimes we have multiple subsystems in one line, as in this example // from: // https://source.datanerd.us/newrelic/cross_agent_tests/blob/master/docker_container_id/docker-1.1.2-native-driver-systemd.txt // // 3:cpuacct,cpu:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope splitCSV := func(r rune) bool { return r == ',' } subsysCPU := []byte("cpu") for _, subsys := range bytes.FieldsFunc(col, splitCSV) { if bytes.Equal(subsysCPU, subsys) { return true } } return false } func isHex(r rune) bool { return ('0' <= r && r <= '9') || ('a' <= r && r <= 'f') } func validateDockerID(id string) error { if len(id) != 64 { return fmt.Errorf("%s is not %d characters long", id, dockerIDLength) } for _, c := range id { if !isHex(c) { return fmt.Errorf("Character: %c is not hex in string %s", c, id) } } return nil } go-agent-3.42.0/v3/internal/sysinfo/docker_test.go000066400000000000000000000027631510742411500220040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "bytes" "path/filepath" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestDockerIDCrossAgent(t *testing.T) { var testCases []struct { File string `json:"filename"` ID string `json:"containerId"` } dir := "docker_container_id" err := crossagent.ReadJSON(filepath.Join(dir, "cases.json"), &testCases) if err != nil { t.Fatal(err) } for _, test := range testCases { file := filepath.Join(dir, test.File) input, err := crossagent.ReadFile(file) if err != nil { t.Error(err) continue } got, _ := parseDockerID(bytes.NewReader(input)) if got != test.ID { mountInfoAttempt, _ := parseDockerIDMountInfo(bytes.NewReader(input)) if mountInfoAttempt != test.ID { t.Errorf("MountInfo Attempt: %s != %s", mountInfoAttempt, test.ID) t.Errorf("Traditional Attempt: %s != %s", got, test.ID) } } } } func TestDockerIDValidation(t *testing.T) { err := validateDockerID("baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1239") if nil != err { t.Error("Validation should pass with a 64-character hex string.") } err = validateDockerID("39ffbba") if nil == err { t.Error("Validation should have failed with short string.") } err = validateDockerID("z000000000000000000000000000000000000000000000000100000000000000") if nil == err { t.Error("Validation should have failed with non-hex characters.") } } go-agent-3.42.0/v3/internal/sysinfo/errors.go000066400000000000000000000004451510742411500210050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "errors" ) var ( // ErrFeatureUnsupported indicates unsupported platform. ErrFeatureUnsupported = errors.New("That feature is not supported on this platform") ) go-agent-3.42.0/v3/internal/sysinfo/hostname.go000066400000000000000000000003241510742411500213030ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo // Hostname returns the host name. func Hostname() (string, error) { return getHostname() } go-agent-3.42.0/v3/internal/sysinfo/hostname_generic.go000066400000000000000000000003451510742411500230020ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !linux // +build !linux package sysinfo import "os" func getHostname() (string, error) { return os.Hostname() } go-agent-3.42.0/v3/internal/sysinfo/hostname_linux.go000066400000000000000000000032771510742411500225340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "os" "syscall" ) func getHostname() (string, error) { // Try the builtin API first, which is designed to match the output of // /bin/hostname, and fallback to uname(2) if that fails to match the // behavior of gethostname(2) as implemented by glibc. On Linux, all // these method should result in the same value because sethostname(2) // limits the hostname to 64 bytes, the same size of the nodename field // returned by uname(2). Note that is correspondence is not true on // other platforms. // // os.Hostname failures should be exceedingly rare, however some systems // configure SELinux to deny read access to /proc/sys/kernel/hostname. // Redhat's OpenShift platform for example. os.Hostname can also fail if // some or all of /proc has been hidden via chroot(2) or manipulation of // the current processes' filesystem namespace via the cgroups APIs. // Docker is an example of a tool that can configure such an // environment. name, err := os.Hostname() if err == nil { return name, nil } var uts syscall.Utsname if err2 := syscall.Uname(&uts); err2 != nil { // The man page documents only one possible error for uname(2), // suggesting that as long as the buffer given is valid, the // call will never fail. Return the original error in the hope // it provides more relevant information about why the hostname // can't be retrieved. return "", err } // Convert Nodename to a Go string. buf := make([]byte, 0, len(uts.Nodename)) for _, c := range uts.Nodename { if c == 0 { break } buf = append(buf, byte(c)) } return string(buf), nil } go-agent-3.42.0/v3/internal/sysinfo/memtotal.go000066400000000000000000000017731510742411500213200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "bufio" "errors" "io" "regexp" "strconv" ) // BytesToMebibytes converts bytes into mebibytes. func BytesToMebibytes(bts uint64) uint64 { return bts / ((uint64)(1024 * 1024)) } var ( meminfoRe = regexp.MustCompile(`^MemTotal:\s+([0-9]+)\s+[kK]B$`) errMemTotalNotFound = errors.New("supported MemTotal not found in /proc/meminfo") ) // parseProcMeminfo is used to parse Linux's "/proc/meminfo". It is located // here so that the relevant cross agent tests will be run on all platforms. func parseProcMeminfo(f io.Reader) (uint64, error) { scanner := bufio.NewScanner(f) for scanner.Scan() { if m := meminfoRe.FindSubmatch(scanner.Bytes()); m != nil { kb, err := strconv.ParseUint(string(m[1]), 10, 64) if err != nil { return 0, err } return kb * 1024, nil } } err := scanner.Err() if err == nil { err = errMemTotalNotFound } return 0, err } go-agent-3.42.0/v3/internal/sysinfo/memtotal_darwin.go000066400000000000000000000012701510742411500226540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "syscall" "unsafe" ) // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { mib := []int32{6 /* CTL_HW */, 24 /* HW_MEMSIZE */} buf := make([]byte, 8) bufLen := uintptr(8) _, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufLen)), uintptr(0), uintptr(0)) if e1 != 0 { return 0, e1 } if bufLen != 8 { return 0, syscall.EIO } return *(*uint64)(unsafe.Pointer(&buf[0])), nil } go-agent-3.42.0/v3/internal/sysinfo/memtotal_darwin_test.go000066400000000000000000000015351510742411500237170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "errors" "os/exec" "regexp" "strconv" "testing" ) var re = regexp.MustCompile(`hw\.memsize:\s*(\d+)`) func darwinSysctlMemoryBytes() (uint64, error) { out, err := exec.Command("/usr/sbin/sysctl", "hw.memsize").Output() if err != nil { return 0, err } match := re.FindSubmatch(out) if match == nil { return 0, errors.New("memory size not found in sysctl output") } bts, err := strconv.ParseUint(string(match[1]), 10, 64) if err != nil { return 0, err } return bts, nil } func TestPhysicalMemoryBytes(t *testing.T) { mem, err := PhysicalMemoryBytes() if err != nil { t.Fatal(err) } mem2, err := darwinSysctlMemoryBytes() if nil != err { t.Fatal(err) } if mem != mem2 { t.Error(mem, mem2) } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_freebsd.go000066400000000000000000000014141510742411500230020ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "syscall" "unsafe" ) // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { mib := []int32{6 /* CTL_HW */, 5 /* HW_PHYSMEM */} buf := make([]byte, 8) bufLen := uintptr(8) _, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufLen)), uintptr(0), uintptr(0)) if e1 != 0 { return 0, e1 } switch bufLen { case 4: return uint64(*(*uint32)(unsafe.Pointer(&buf[0]))), nil case 8: return *(*uint64)(unsafe.Pointer(&buf[0])), nil default: return 0, syscall.EIO } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_freebsd_test.go000066400000000000000000000015331510742411500240430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "errors" "os/exec" "regexp" "strconv" "testing" ) var re = regexp.MustCompile(`hw\.physmem:\s*(\d+)`) func freebsdSysctlMemoryBytes() (uint64, error) { out, err := exec.Command("/sbin/sysctl", "hw.physmem").Output() if err != nil { return 0, err } match := re.FindSubmatch(out) if match == nil { return 0, errors.New("memory size not found in sysctl output") } bts, err := strconv.ParseUint(string(match[1]), 10, 64) if err != nil { return 0, err } return bts, nil } func TestPhysicalMemoryBytes(t *testing.T) { mem, err := PhysicalMemoryBytes() if err != nil { t.Fatal(err) } mem2, err := freebsdSysctlMemoryBytes() if nil != err { t.Fatal(err) } if mem != mem2 { t.Error(mem, mem2) } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_js.go000066400000000000000000000004511510742411500220040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import "errors" // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { return 0, errors.New("not supported on GOOS=js") } go-agent-3.42.0/v3/internal/sysinfo/memtotal_linux.go000066400000000000000000000005511510742411500225300ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import "os" // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { f, err := os.Open("/proc/meminfo") if err != nil { return 0, err } defer f.Close() return parseProcMeminfo(f) } go-agent-3.42.0/v3/internal/sysinfo/memtotal_openbsd_amd64.go000066400000000000000000000014171510742411500240200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "syscall" "unsafe" ) // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { mib := []int32{6 /* CTL_HW */, 19 /* HW_PHYSMEM64 */} buf := make([]byte, 8) bufLen := uintptr(8) _, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufLen)), uintptr(0), uintptr(0)) if e1 != 0 { return 0, e1 } switch bufLen { case 4: return uint64(*(*uint32)(unsafe.Pointer(&buf[0]))), nil case 8: return *(*uint64)(unsafe.Pointer(&buf[0])), nil default: return 0, syscall.EIO } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_openbsd_test.go000066400000000000000000000015621510742411500240650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "errors" "os/exec" "regexp" "strconv" "testing" ) var re = regexp.MustCompile(`hw\.physmem=(\d+)`) func openbsdSysctlMemoryBytes() (uint64, error) { out, err := exec.Command("/sbin/sysctl", "hw.physmem").Output() if err != nil { return 0, err } match := re.FindSubmatch(out) if match == nil { return 0, errors.New("memory size not found in sysctl output") } bts, err := strconv.ParseUint(string(match[1]), 10, 64) if err != nil { return 0, err } return bts, nil } func TestPhysicalMemoryBytes(t *testing.T) { mem, err := PhysicalMemoryBytes() if err != nil { t.Fatal(err) } mem2, err := openbsdSysctlMemoryBytes() if nil != err { t.Fatal(err) } if mem != mem2 { t.Errorf("Expected %d, got %d\n", mem2, mem) } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_solaris.go000066400000000000000000000011561510742411500230470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo /* #include */ import "C" // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { // The function we're calling on Solaris is // long sysconf(int name); var pages C.long var pagesizeBytes C.long var err error pagesizeBytes, err = C.sysconf(C._SC_PAGE_SIZE) if pagesizeBytes < 1 { return 0, err } pages, err = C.sysconf(C._SC_PHYS_PAGES) if pages < 1 { return 0, err } return uint64(pages) * uint64(pagesizeBytes), nil } go-agent-3.42.0/v3/internal/sysinfo/memtotal_solaris_test.go000066400000000000000000000024511510742411500241050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "errors" "os/exec" "regexp" "strconv" "strings" "testing" ) func TestPhysicalMemoryBytes(t *testing.T) { prtconf, err := prtconfMemoryBytes() if err != nil { t.Fatal(err) } sysconf, err := PhysicalMemoryBytes() if err != nil { t.Fatal(err) } // The pagesize*pages calculation, although standard (the JVM, at least, // uses this approach), doesn't match up exactly with the number // returned by prtconf. if sysconf > prtconf || sysconf < (prtconf-prtconf/20) { t.Fatal(prtconf, sysconf) } } var ( ptrconfRe = regexp.MustCompile(`[Mm]emory\s*size:\s*([0-9]+)\s*([a-zA-Z]+)`) ) func prtconfMemoryBytes() (uint64, error) { output, err := exec.Command("/usr/sbin/prtconf").Output() if err != nil { return 0, err } m := ptrconfRe.FindSubmatch(output) if m == nil { return 0, errors.New("memory size not found in prtconf output") } size, err := strconv.ParseUint(string(m[1]), 10, 64) if err != nil { return 0, err } switch strings.ToLower(string(m[2])) { case "megabytes", "mb": return size * 1024 * 1024, nil case "kilobytes", "kb": return size * 1024, nil default: return 0, errors.New("couldn't parse memory size in prtconf output") } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_test.go000066400000000000000000000020521510742411500223460ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "os" "regexp" "strconv" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestMemTotal(t *testing.T) { var fileRe = regexp.MustCompile(`meminfo_([0-9]+)MB.txt$`) var ignoreFile = regexp.MustCompile(`README\.md$`) testCases, err := crossagent.ReadDir("proc_meminfo") if err != nil { t.Fatal(err) } for _, testFile := range testCases { if ignoreFile.MatchString(testFile) { continue } matches := fileRe.FindStringSubmatch(testFile) if matches == nil || len(matches) < 2 { t.Error(testFile, matches) continue } expect, err := strconv.ParseUint(matches[1], 10, 64) if err != nil { t.Error(err) continue } input, err := os.Open(testFile) if err != nil { t.Error(err) continue } bts, err := parseProcMeminfo(input) input.Close() mib := BytesToMebibytes(bts) if err != nil { t.Error(err) } else if mib != expect { t.Error(bts, expect) } } } go-agent-3.42.0/v3/internal/sysinfo/memtotal_windows.go000066400000000000000000000013421510742411500230620ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "syscall" "unsafe" ) // PhysicalMemoryBytes returns the total amount of host memory. func PhysicalMemoryBytes() (uint64, error) { // https://msdn.microsoft.com/en-us/library/windows/desktop/cc300158(v=vs.85).aspx // http://stackoverflow.com/questions/30743070/query-total-physical-memory-in-windows-with-golang mod := syscall.NewLazyDLL("kernel32.dll") proc := mod.NewProc("GetPhysicallyInstalledSystemMemory") var memkb uint64 ret, _, err := proc.Call(uintptr(unsafe.Pointer(&memkb))) // return value TRUE(1) succeeds, FAILED(0) fails if ret != 1 { return 0, err } return memkb * 1024, nil } go-agent-3.42.0/v3/internal/sysinfo/usage.go000066400000000000000000000003661510742411500205770ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "time" ) // Usage contains process process times. type Usage struct { System time.Duration User time.Duration } go-agent-3.42.0/v3/internal/sysinfo/usage_js.go000066400000000000000000000004061510742411500212660ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import "errors" // GetUsage gathers process times. func GetUsage() (Usage, error) { return Usage{}, errors.New("not supported on GOOS=js") } go-agent-3.42.0/v3/internal/sysinfo/usage_posix.go000066400000000000000000000011341510742411500220130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !windows && !js // +build !windows,!js package sysinfo import ( "syscall" "time" ) func timevalToDuration(tv syscall.Timeval) time.Duration { return time.Duration(tv.Nano()) * time.Nanosecond } // GetUsage gathers process times. func GetUsage() (Usage, error) { ru := syscall.Rusage{} err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru) if err != nil { return Usage{}, err } return Usage{ System: timevalToDuration(ru.Stime), User: timevalToDuration(ru.Utime), }, nil } go-agent-3.42.0/v3/internal/sysinfo/usage_windows.go000066400000000000000000000014301510742411500223420ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sysinfo import ( "syscall" "time" ) func filetimeToDuration(ft *syscall.Filetime) time.Duration { ns := ft.Nanoseconds() return time.Duration(ns) } // GetUsage gathers process times. func GetUsage() (Usage, error) { var creationTime syscall.Filetime var exitTime syscall.Filetime var kernelTime syscall.Filetime var userTime syscall.Filetime handle, err := syscall.GetCurrentProcess() if err != nil { return Usage{}, err } err = syscall.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime) if err != nil { return Usage{}, err } return Usage{ System: filetimeToDuration(&kernelTime), User: filetimeToDuration(&userTime), }, nil } go-agent-3.42.0/v3/internal/tools/000077500000000000000000000000001510742411500166055ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/tools/interface-wrapping/000077500000000000000000000000001510742411500223725ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/tools/interface-wrapping/driver_conn.json000066400000000000000000000006341510742411500256000ustar00rootroot00000000000000{ "comment": "used in wrapping driver.Conn", "variable_name": "conn", "test_variable_name": "conn.original", "required_interfaces": [ "driver.Conn" ], "optional_interfaces": [ "driver.ConnBeginTx", "driver.ConnPrepareContext", "driver.Execer", "driver.ExecerContext", "driver.NamedValueChecker", "driver.Pinger", "driver.Queryer", "driver.QueryerContext", "driver.SessionResetter" ] } go-agent-3.42.0/v3/internal/tools/interface-wrapping/driver_driver.json000066400000000000000000000003261510742411500261340ustar00rootroot00000000000000{ "comment": "used in wrapping driver.Driver", "variable_name": "dv", "test_variable_name": "dv.original", "required_interfaces": [ "driver.Driver" ], "optional_interfaces": [ "driver.DriverContext" ] } go-agent-3.42.0/v3/internal/tools/interface-wrapping/driver_stmt.json000066400000000000000000000004571510742411500256350ustar00rootroot00000000000000{ "comment": "used in wrapping driver.Stmt", "variable_name": "stmt", "test_variable_name": "stmt.original", "required_interfaces": [ "driver.Stmt" ], "optional_interfaces": [ "driver.ColumnConverter", "driver.NamedValueChecker", "driver.StmtExecContext", "driver.StmtQueryContext" ] } go-agent-3.42.0/v3/internal/tools/interface-wrapping/main.go000066400000000000000000000067141510742411500236550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "fmt" "io/ioutil" "os" ) // This program is generates code for wrapping interfaces which implement // optional interfaces. For some context on the problem this solves, read: // https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html // This problem takes one of the json files in this directory as input: // eg. go run main.go transaction_response_writer.json func main() { if len(os.Args) < 2 { fmt.Println("provide input file") os.Exit(1) } filename := os.Args[1] inputBytes, err := ioutil.ReadFile(filename) if nil != err { fmt.Println(fmt.Errorf("unable to read %v: %v", filename, err)) os.Exit(1) } var input struct { // variableName must implement all of the required interfaces // and all of the optional interfaces. It will be used to // populate the fields of anonymous structs which have // interfaces embedded. VariableName string `json:"variable_name"` // variableName is the variable that will be tested against the // optional interfaces. It is the "thing being wrapped" whose // behavior we seek to emulate. TestVariableName string `json:"test_variable_name"` RequiresInterfaces []string `json:"required_interfaces"` OptionalInterfaces []string `json:"optional_interfaces"` } err = json.Unmarshal(inputBytes, &input) if nil != err { fmt.Println(fmt.Errorf("unable to unmarshal input: %v", err)) os.Exit(1) } bitflagVariables := make([]string, len(input.OptionalInterfaces)) for idx := range input.OptionalInterfaces { bitflagVariables[idx] = fmt.Sprintf("i%d", idx) } fmt.Println("// GENERATED CODE DO NOT MODIFY") fmt.Println("// This code generated by internal/tools/interface-wrapping") fmt.Println("var (") for idx := range input.OptionalInterfaces { fmt.Println(fmt.Sprintf("%s int32 = 1 << %d", bitflagVariables[idx], idx)) } fmt.Println(")") // interfaceSet is a bitset whose value represents the optional // interfaces that $input.TestVariableName implements. fmt.Println("var interfaceSet int32") for idx, inter := range input.OptionalInterfaces { fmt.Println(fmt.Sprintf("if _, ok := %s.(%s); ok {", input.TestVariableName, inter)) fmt.Println(fmt.Sprintf("interfaceSet |= %s", bitflagVariables[idx])) fmt.Println("}") } permutations := make([][]int, 1< 0 { cs += " | " } cs += bitflagVariables[elem] } if cs == "" { fmt.Println("default: // No optional interfaces implemented") } else { fmt.Println(fmt.Sprintf("case %s:", cs)) } fmt.Println("return struct {") for _, required := range input.RequiresInterfaces { fmt.Println(required) } for _, elem := range permutation { fmt.Println(input.OptionalInterfaces[elem]) } totalImplements := len(input.RequiresInterfaces) + len(permutation) var varList string for i := 0; i < totalImplements; i++ { if i > 0 { varList += ", " } varList += input.VariableName } fmt.Println("} { " + varList + " }") } fmt.Println("}") } go-agent-3.42.0/v3/internal/tools/interface-wrapping/response_writer.json000066400000000000000000000004271510742411500265220ustar00rootroot00000000000000{ "comment": "used in internal_response_writer.go", "variable_name": "rw", "test_variable_name": "rw.original", "required_interfaces": [ "http.ResponseWriter" ], "optional_interfaces": [ "http.CloseNotifier", "http.Flusher", "http.Hijacker", "io.ReaderFrom" ] } go-agent-3.42.0/v3/internal/tools/rules/000077500000000000000000000000001510742411500177375ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/tools/rules/main.go000066400000000000000000000020661510742411500212160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "fmt" "io/ioutil" "os" "github.com/newrelic/go-agent/v3/internal" ) func fail(reason string) { fmt.Println(reason) os.Exit(1) } func main() { if len(os.Args) < 3 { fail("improper usage: ./rules path/to/reply_file input") } connectReplyFile := os.Args[1] name := os.Args[2] data, err := ioutil.ReadFile(connectReplyFile) if nil != err { fail(fmt.Sprintf("unable to open '%s': %s", connectReplyFile, err)) } var reply internal.ConnectReply err = json.Unmarshal(data, &reply) if nil != err { fail(fmt.Sprintf("unable unmarshal reply: %s", err)) } // Metric Rules out := reply.MetricRules.Apply(name) fmt.Println("metric rules applied:", out) // Url Rules + Txn Name Rules + Segment Term Rules out = internal.CreateFullTxnName(name, &reply, true) fmt.Println("treated as web txn name:", out) out = internal.CreateFullTxnName(name, &reply, false) fmt.Println("treated as backround txn name:", out) } go-agent-3.42.0/v3/internal/trace_id_generator.go000066400000000000000000000030211510742411500216100ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "math/rand" "sync" ) // TraceIDGenerator creates identifiers for distributed tracing. type TraceIDGenerator struct { sync.Mutex rnd *rand.Rand } // NewTraceIDGenerator creates a new trace identifier generator. func NewTraceIDGenerator(seed int64) *TraceIDGenerator { return &TraceIDGenerator{ rnd: rand.New(rand.NewSource(seed)), } } // Float32 returns a random float32 from its random source. func (tg *TraceIDGenerator) Float32() float32 { tg.Lock() defer tg.Unlock() return tg.rnd.Float32() } const ( traceIDByteLen = 16 // TraceIDHexStringLen is the length of the trace ID when represented // as a hex string. TraceIDHexStringLen = 32 spanIDByteLen = 8 maxIDByteLen = 16 ) const ( hextable = "0123456789abcdef" ) // GenerateTraceID creates a new trace identifier, which is a 32 character hex string. func (tg *TraceIDGenerator) GenerateTraceID() string { return tg.generateID(traceIDByteLen) } // GenerateSpanID creates a new span identifier, which is a 16 character hex string. func (tg *TraceIDGenerator) GenerateSpanID() string { return tg.generateID(spanIDByteLen) } func (tg *TraceIDGenerator) generateID(len int) string { var bits [maxIDByteLen * 2]byte tg.Lock() defer tg.Unlock() tg.rnd.Read(bits[:len]) // In-place encode for i := len - 1; i >= 0; i-- { bits[i*2+1] = hextable[bits[i]&0x0f] bits[i*2] = hextable[bits[i]>>4] } return string(bits[:len*2]) } go-agent-3.42.0/v3/internal/trace_id_generator_test.go000066400000000000000000000016471510742411500226630ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import "testing" func TestTraceIDGenerator(t *testing.T) { tg := NewTraceIDGenerator(12345) traceID := tg.GenerateTraceID() if traceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error(traceID) } spanID := tg.GenerateSpanID() if spanID != "e71870997d38ef60" { t.Error(spanID) } if p := tg.Float32(); p != 0.05700199 { t.Error(p) } } func BenchmarkTraceIDGenerator(b *testing.B) { tg := NewTraceIDGenerator(12345) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { if id := tg.GenerateSpanID(); id == "" { b.Fatal(id) } } } func BenchmarkTraceIDGeneratorParallel(b *testing.B) { tg := NewTraceIDGenerator(12345) b.ResetTimer() b.ReportAllocs() b.RunParallel(func(p *testing.PB) { for p.Next() { if id := tg.GenerateSpanID(); id == "" { b.Fatal(id) } } }) } go-agent-3.42.0/v3/internal/usage.go000066400000000000000000000013331510742411500171000ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "strings" "sync" ) var ( trackMutex sync.Mutex trackMetrics []string ) // TrackUsage helps track which integration packages are used. func TrackUsage(s ...string) { trackMutex.Lock() defer trackMutex.Unlock() m := "Supportability/" + strings.Join(s, "/") trackMetrics = append(trackMetrics, m) } // GetUsageSupportabilityMetrics returns supportability metric names. func GetUsageSupportabilityMetrics() []string { trackMutex.Lock() defer trackMutex.Unlock() names := make([]string, 0, len(trackMetrics)) for _, s := range trackMetrics { names = append(names, s) } return names } go-agent-3.42.0/v3/internal/utilities.go000066400000000000000000000021271510742411500200110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "bytes" "encoding/json" "fmt" "reflect" "runtime" "time" ) // FloatSecondsToDuration turns a float64 in seconds into a time.Duration. func FloatSecondsToDuration(seconds float64) time.Duration { nanos := seconds * 1000 * 1000 * 1000 return time.Duration(nanos) * time.Nanosecond } // CompactJSONString removes the whitespace from a JSON string. This function // will panic if the string provided is not valid JSON. Thus is must only be // used in testing code! func CompactJSONString(js string) string { buf := new(bytes.Buffer) if err := json.Compact(buf, []byte(js)); err != nil { panic(fmt.Errorf("unable to compact JSON: %v", err)) } return buf.String() } // HandlerName return name of a function. func HandlerName(h interface{}) string { if h == nil { return "" } t := reflect.ValueOf(h).Type() if t.Kind() == reflect.Func { if pointer := runtime.FuncForPC(reflect.ValueOf(h).Pointer()); pointer != nil { return pointer.Name() } } return "" } go-agent-3.42.0/v3/internal/utilities_test.go000066400000000000000000000007721510742411500210540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package internal import ( "testing" "time" ) func TestFloatSecondsToDuration(t *testing.T) { if d := FloatSecondsToDuration(0.123); d != 123*time.Millisecond { t.Error(d) } if d := FloatSecondsToDuration(456.0); d != 456*time.Second { t.Error(d) } } func TestCompactJSON(t *testing.T) { in := ` { "zip": 1}` out := CompactJSONString(in) if out != `{"zip":1}` { t.Fatal(in, out) } } go-agent-3.42.0/v3/internal/utilization/000077500000000000000000000000001510742411500200205ustar00rootroot00000000000000go-agent-3.42.0/v3/internal/utilization/addresses.go000066400000000000000000000040041510742411500223220ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "fmt" "net" ) func nonlocalIPAddressesByInterface() (map[string][]string, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } ips := make(map[string][]string, len(ifaces)) for _, ifc := range ifaces { addrs, err := ifc.Addrs() if err != nil { continue } for _, addr := range addrs { var ip net.IP switch iptype := addr.(type) { case *net.IPAddr: ip = iptype.IP case *net.IPNet: ip = iptype.IP case *net.TCPAddr: ip = iptype.IP case *net.UDPAddr: ip = iptype.IP } if nil != ip && !ip.IsLoopback() && !ip.IsUnspecified() { ips[ifc.Name] = append(ips[ifc.Name], ip.String()) } } } return ips, nil } // utilizationIPs gathers IP address which may help identify this entity. This // code chooses all IPs from the interface which contains the IP of a UDP // connection with NR. This approach has the following advantages: // * Matches the behavior of the Java agent. // * Reports fewer IPs to lower linking burden on infrastructure backend. // * The UDP connection interface is more likely to contain unique external IPs. func utilizationIPs() ([]string, error) { // Port choice designed to match // https://github.com/newrelic/newrelic-java-agent/blob/main/newrelic-agent/src/main/java/com/newrelic/agent/config/Hostname.java#L120 conn, err := net.Dial("udp", "collector.newrelic.com:10002") if err != nil { return nil, err } defer conn.Close() addr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok || nil == addr || addr.IP.IsLoopback() || addr.IP.IsUnspecified() { return nil, fmt.Errorf("unexpected connection address: %v", conn.LocalAddr()) } outboundIP := addr.IP.String() ipsByInterface, err := nonlocalIPAddressesByInterface() if err != nil { return nil, err } for _, ips := range ipsByInterface { for _, ip := range ips { if ip == outboundIP { return ips, nil } } } return nil, nil } go-agent-3.42.0/v3/internal/utilization/aws.go000066400000000000000000000071451510742411500211500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" ) const ( awsHostname = "169.254.169.254" awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document" awsTokenEndpointPath = "/latest/api/token" awsEndpoint = "http://" + awsHostname + awsEndpointPath awsTokenEndpoint = "http://" + awsHostname + awsTokenEndpointPath awsTokenTTL = "60" // seconds this AWS utilization session will last ) type aws struct { InstanceID string `json:"instanceId,omitempty"` InstanceType string `json:"instanceType,omitempty"` AvailabilityZone string `json:"availabilityZone,omitempty"` } func gatherAWS(util *Data, client *http.Client) error { aws, err := getAWS(client) if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running AWS about a timeout. if _, ok := err.(unexpectedAWSErr); ok { return err } return nil } util.Vendors.AWS = aws return nil } type unexpectedAWSErr struct{ e error } func (e unexpectedAWSErr) Error() string { return fmt.Sprintf("unexpected AWS error: %v", e.e) } // getAWSToken attempts to get the IMDSv2 token within the providerTimeout set // provider.go. func getAWSToken(client *http.Client) (token string, err error) { request, err := http.NewRequest("PUT", awsTokenEndpoint, nil) request.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", awsTokenTTL) response, err := client.Do(request) if err != nil { return "", err } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { return "", err } return string(body), nil } func getAWS(client *http.Client) (ret *aws, err error) { // In some cases, 3rd party providers might block requests to metadata // endpoints in such a way that causes a panic in the underlying // net/http library's (*Transport).getConn() function. To mitigate that // possibility, we preemptively setup a recovery deferral. defer func() { if r := recover(); r != nil { ret = nil err = unexpectedAWSErr{e: errors.New("panic contacting AWS metadata endpoint")} } }() // AWS' IMDSv2 requires us to get a token before requesting metadata. awsToken, err := getAWSToken(client) if err != nil { // No unexpectedAWSErr here: A timeout is usually going to // happen. return nil, err } //Add the header to the outbound request. request, err := http.NewRequest("GET", awsEndpoint, nil) request.Header.Add("X-aws-ec2-metadata-token", awsToken) response, err := client.Do(request) if err != nil { // No unexpectedAWSErr here: A timeout is usually going to // happen. return nil, err } defer response.Body.Close() if response.StatusCode != 200 { return nil, unexpectedAWSErr{e: fmt.Errorf("response code %d", response.StatusCode)} } data, err := ioutil.ReadAll(response.Body) if err != nil { return nil, unexpectedAWSErr{e: err} } a := &aws{} if err := json.Unmarshal(data, a); err != nil { return nil, unexpectedAWSErr{e: err} } if err := a.validate(); err != nil { return nil, unexpectedAWSErr{e: err} } return a, nil } func (a *aws) validate() (err error) { a.InstanceID, err = normalizeValue(a.InstanceID) if err != nil { return fmt.Errorf("invalid instance ID: %v", err) } a.InstanceType, err = normalizeValue(a.InstanceType) if err != nil { return fmt.Errorf("invalid instance type: %v", err) } a.AvailabilityZone, err = normalizeValue(a.AvailabilityZone) if err != nil { return fmt.Errorf("invalid availability zone: %v", err) } return } go-agent-3.42.0/v3/internal/utilization/aws_test.go000066400000000000000000000030271510742411500222020ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestCrossAgentAWS(t *testing.T) { var testCases []testCase err := crossagent.ReadJSON("utilization_vendor_specific/aws.json", &testCases) if err != nil { t.Fatalf("reading aws.json failed: %v", err) } for _, testCase := range testCases { client := &http.Client{ Transport: &mockTransport{ t: t, responses: testCase.URIs, }, } aws, err := getAWS(client) if testCase.ExpectedVendorsHash.AWS == nil { if err == nil { t.Fatalf("%s: expected error; got nil", testCase.TestName) } } else { if err != nil { t.Fatalf("%s: expected no error; got %v", testCase.TestName, err) } if aws.InstanceID != testCase.ExpectedVendorsHash.AWS.InstanceID { t.Fatalf("%s: instanceId incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.AWS.InstanceID, aws.InstanceID) } if aws.InstanceType != testCase.ExpectedVendorsHash.AWS.InstanceType { t.Fatalf("%s: instanceType incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.AWS.InstanceType, aws.InstanceType) } if aws.AvailabilityZone != testCase.ExpectedVendorsHash.AWS.AvailabilityZone { t.Fatalf("%s: availabilityZone incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.AWS.AvailabilityZone, aws.AvailabilityZone) } } } } go-agent-3.42.0/v3/internal/utilization/azure.go000066400000000000000000000050261510742411500215000ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "encoding/json" "fmt" "io/ioutil" "net/http" ) const ( azureHostname = "169.254.169.254" azureEndpointPath = "/metadata/instance/compute?api-version=2017-03-01" azureEndpoint = "http://" + azureHostname + azureEndpointPath ) type azure struct { Location string `json:"location,omitempty"` Name string `json:"name,omitempty"` VMID string `json:"vmId,omitempty"` VMSize string `json:"vmSize,omitempty"` } func gatherAzure(util *Data, client *http.Client) error { az, err := getAzure(client) if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running Azure about a timeout. // If any of the other vendors have already been detected and set, and we have an error, we should not return the error // If no vendors have been detected, we should return the error. if _, ok := err.(unexpectedAzureErr); ok && !util.Vendors.AnySet() { return err } return nil } util.Vendors.Azure = az return nil } type unexpectedAzureErr struct{ e error } func (e unexpectedAzureErr) Error() string { return fmt.Sprintf("unexpected Azure error: %v", e.e) } func getAzure(client *http.Client) (*azure, error) { req, err := http.NewRequest("GET", azureEndpoint, nil) if err != nil { return nil, err } req.Header.Add("Metadata", "true") response, err := client.Do(req) if err != nil { // No unexpectedAzureErr here: a timeout isusually going to // happen. return nil, err } defer response.Body.Close() if response.StatusCode != 200 { return nil, unexpectedAzureErr{e: fmt.Errorf("response code %d", response.StatusCode)} } data, err := ioutil.ReadAll(response.Body) if err != nil { return nil, unexpectedAzureErr{e: err} } az := &azure{} if err := json.Unmarshal(data, az); err != nil { return nil, unexpectedAzureErr{e: err} } if err := az.validate(); err != nil { return nil, unexpectedAzureErr{e: err} } return az, nil } func (az *azure) validate() (err error) { az.Location, err = normalizeValue(az.Location) if err != nil { return fmt.Errorf("Invalid location: %v", err) } az.Name, err = normalizeValue(az.Name) if err != nil { return fmt.Errorf("Invalid name: %v", err) } az.VMID, err = normalizeValue(az.VMID) if err != nil { return fmt.Errorf("Invalid VM ID: %v", err) } az.VMSize, err = normalizeValue(az.VMSize) if err != nil { return fmt.Errorf("Invalid VM size: %v", err) } return } go-agent-3.42.0/v3/internal/utilization/azure_test.go000066400000000000000000000032351510742411500225370ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestCrossAgentAzure(t *testing.T) { var testCases []testCase err := crossagent.ReadJSON("utilization_vendor_specific/azure.json", &testCases) if err != nil { t.Fatalf("reading azure.json failed: %v", err) } for _, testCase := range testCases { client := &http.Client{ Transport: &mockTransport{ t: t, responses: testCase.URIs, }, } azure, err := getAzure(client) if testCase.ExpectedVendorsHash.Azure == nil { if err == nil { t.Fatalf("%s: expected error; got nil", testCase.TestName) } } else { if err != nil { t.Fatalf("%s: expected no error; got %v", testCase.TestName, err) } if azure.Location != testCase.ExpectedVendorsHash.Azure.Location { t.Fatalf("%s: Location incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.Azure.Location, azure.Location) } if azure.Name != testCase.ExpectedVendorsHash.Azure.Name { t.Fatalf("%s: Name incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.Azure.Name, azure.Name) } if azure.VMID != testCase.ExpectedVendorsHash.Azure.VMID { t.Fatalf("%s: VMID incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.Azure.VMID, azure.VMID) } if azure.VMSize != testCase.ExpectedVendorsHash.Azure.VMSize { t.Fatalf("%s: VMSize incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.Azure.VMSize, azure.VMSize) } } } } go-agent-3.42.0/v3/internal/utilization/fqdn.go000066400000000000000000000011221510742411500212730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.8 // +build go1.8 package utilization import ( "context" "net" "strings" ) func lookupAddr(addr string) ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), lookupAddrTimeout) defer cancel() r := &net.Resolver{} return r.LookupAddr(ctx, addr) } func getFQDN(candidateIPs []string) string { for _, ip := range candidateIPs { names, _ := lookupAddr(ip) if len(names) > 0 { return strings.TrimSuffix(names[0], ".") } } return "" } go-agent-3.42.0/v3/internal/utilization/fqdn_pre18.go000066400000000000000000000006041510742411500223160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !go1.8 // +build !go1.8 package utilization // net.Resolver.LookupAddr was added in Go 1.8, and net.LookupAddr does not have // a controllable timeout, so we skip the optional full_hostname on pre 1.8 // versions. func getFQDN(candidateIPs []string) string { return "" } go-agent-3.42.0/v3/internal/utilization/gcp.go000066400000000000000000000071551510742411500211300ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "encoding/json" "fmt" "io/ioutil" "net/http" "strings" ) const ( gcpHostname = "metadata.google.internal" gcpEndpointPath = "/computeMetadata/v1/instance/?recursive=true" gcpEndpoint = "http://" + gcpHostname + gcpEndpointPath ) func gatherGCP(util *Data, client *http.Client) error { gcp, err := getGCP(client) if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running GCP about a timeout. if _, ok := err.(unexpectedGCPErr); ok { return err } return nil } util.Vendors.GCP = gcp return nil } // numericString is used rather than json.Number because we want the output when // marshalled to be a string, rather than a number. type numericString string func (ns *numericString) MarshalJSON() ([]byte, error) { return json.Marshal(ns.String()) } func (ns *numericString) String() string { return string(*ns) } func (ns *numericString) UnmarshalJSON(data []byte) error { var n int64 // Try to unmarshal as an integer first. if err := json.Unmarshal(data, &n); err == nil { *ns = numericString(fmt.Sprintf("%d", n)) return nil } // Otherwise, unmarshal as a string, and verify that it's numeric (for our // definition of numeric, which is actually integral). var s string if err := json.Unmarshal(data, &s); err != nil { return err } for _, r := range s { if r < '0' || r > '9' { return fmt.Errorf("invalid numeric character: %c", r) } } *ns = numericString(s) return nil } type gcp struct { ID numericString `json:"id"` MachineType string `json:"machineType,omitempty"` Name string `json:"name,omitempty"` Zone string `json:"zone,omitempty"` } type unexpectedGCPErr struct{ e error } func (e unexpectedGCPErr) Error() string { return fmt.Sprintf("unexpected GCP error: %v", e.e) } func getGCP(client *http.Client) (*gcp, error) { // GCP's metadata service requires a Metadata-Flavor header because... hell, I // don't know, maybe they really like Guy Fieri? req, err := http.NewRequest("GET", gcpEndpoint, nil) if err != nil { return nil, err } req.Header.Add("Metadata-Flavor", "Google") response, err := client.Do(req) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != 200 { return nil, unexpectedGCPErr{e: fmt.Errorf("response code %d", response.StatusCode)} } data, err := ioutil.ReadAll(response.Body) if err != nil { return nil, unexpectedGCPErr{e: err} } g := &gcp{} if err := json.Unmarshal(data, g); err != nil { return nil, unexpectedGCPErr{e: err} } if err := g.validate(); err != nil { return nil, unexpectedGCPErr{e: err} } return g, nil } func (g *gcp) validate() (err error) { id, err := normalizeValue(g.ID.String()) if err != nil { return fmt.Errorf("Invalid ID: %v", err) } g.ID = numericString(id) mt, err := normalizeValue(g.MachineType) if err != nil { return fmt.Errorf("Invalid machine type: %v", err) } g.MachineType = stripGCPPrefix(mt) g.Name, err = normalizeValue(g.Name) if err != nil { return fmt.Errorf("Invalid name: %v", err) } zone, err := normalizeValue(g.Zone) if err != nil { return fmt.Errorf("Invalid zone: %v", err) } g.Zone = stripGCPPrefix(zone) return } // We're only interested in the last element of slash separated paths for the // machine type and zone values, so this function handles stripping the parts // we don't need. func stripGCPPrefix(s string) string { parts := strings.Split(s, "/") return parts[len(parts)-1] } go-agent-3.42.0/v3/internal/utilization/gcp_test.go000066400000000000000000000037671510742411500221740ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestCrossAgentGCP(t *testing.T) { var testCases []testCase err := crossagent.ReadJSON("utilization_vendor_specific/gcp.json", &testCases) if err != nil { t.Fatalf("reading gcp.json failed: %v", err) } for _, testCase := range testCases { client := &http.Client{ Transport: &mockTransport{ t: t, responses: testCase.URIs, }, } gcp, err := getGCP(client) if testCase.ExpectedVendorsHash.GCP == nil { if err == nil { t.Fatalf("%s: expected error; got nil", testCase.TestName) } } else { if err != nil { t.Fatalf("%s: expected no error; got %v", testCase.TestName, err) } if gcp.ID != testCase.ExpectedVendorsHash.GCP.ID { t.Fatalf("%s: ID incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.GCP.ID, gcp.ID) } if gcp.MachineType != testCase.ExpectedVendorsHash.GCP.MachineType { t.Fatalf("%s: MachineType incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.GCP.MachineType, gcp.MachineType) } if gcp.Name != testCase.ExpectedVendorsHash.GCP.Name { t.Fatalf("%s: Name incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.GCP.Name, gcp.Name) } if gcp.Zone != testCase.ExpectedVendorsHash.GCP.Zone { t.Fatalf("%s: Zone incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.GCP.Zone, gcp.Zone) } } } } func TestStripGCPPrefix(t *testing.T) { testCases := []struct { input string expected string }{ {"foo/bar", "bar"}, {"/foo/bar", "bar"}, {"/foo/bar/", ""}, {"foo", "foo"}, {"", ""}, } for _, tc := range testCases { actual := stripGCPPrefix(tc.input) if tc.expected != actual { t.Fatalf("input: %s; expected: %s; actual: %s", tc.input, tc.expected, actual) } } } go-agent-3.42.0/v3/internal/utilization/pcf.go000066400000000000000000000036261510742411500211260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "errors" "fmt" "net/http" "os" ) type pcf struct { InstanceGUID string `json:"cf_instance_guid,omitempty"` InstanceIP string `json:"cf_instance_ip,omitempty"` MemoryLimit string `json:"memory_limit,omitempty"` } func gatherPCF(util *Data, _ *http.Client) error { pcf, err := getPCF(os.Getenv) if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running PCF about a timeout. if _, ok := err.(unexpectedPCFErr); ok { return err } return nil } util.Vendors.PCF = pcf return nil } type unexpectedPCFErr struct{ e error } func (e unexpectedPCFErr) Error() string { return fmt.Sprintf("unexpected PCF error: %v", e.e) } var ( errNoPCFVariables = errors.New("no PCF environment variables present") ) func getPCF(initializer func(key string) string) (*pcf, error) { p := &pcf{} p.InstanceGUID = initializer("CF_INSTANCE_GUID") p.InstanceIP = initializer("CF_INSTANCE_IP") p.MemoryLimit = initializer("MEMORY_LIMIT") if "" == p.InstanceGUID && "" == p.InstanceIP && "" == p.MemoryLimit { return nil, errNoPCFVariables } if err := p.validate(); err != nil { return nil, unexpectedPCFErr{e: err} } return p, nil } func (pcf *pcf) validate() (err error) { pcf.InstanceGUID, err = normalizeValue(pcf.InstanceGUID) if err != nil { return fmt.Errorf("Invalid instance GUID: %v", err) } pcf.InstanceIP, err = normalizeValue(pcf.InstanceIP) if err != nil { return fmt.Errorf("Invalid instance IP: %v", err) } pcf.MemoryLimit, err = normalizeValue(pcf.MemoryLimit) if err != nil { return fmt.Errorf("Invalid memory limit: %v", err) } if pcf.InstanceGUID == "" || pcf.InstanceIP == "" || pcf.MemoryLimit == "" { err = errors.New("One or more environment variables are unavailable") } return } go-agent-3.42.0/v3/internal/utilization/pcf_test.go000066400000000000000000000027671510742411500221720ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestCrossAgentPCF(t *testing.T) { var testCases []testCase err := crossagent.ReadJSON("utilization_vendor_specific/pcf.json", &testCases) if err != nil { t.Fatalf("reading pcf.json failed: %v", err) } for _, testCase := range testCases { pcf, err := getPCF(func(key string) string { resp := testCase.EnvVars[key] if resp.Timeout { return "" } return resp.Response }) if testCase.ExpectedVendorsHash.PCF == nil { if err == nil { t.Fatalf("%s: expected error; got nil", testCase.TestName) } } else { if err != nil { t.Fatalf("%s: expected no error; got %v", testCase.TestName, err) } if pcf.InstanceGUID != testCase.ExpectedVendorsHash.PCF.InstanceGUID { t.Fatalf("%s: InstanceGUID incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.PCF.InstanceGUID, pcf.InstanceGUID) } if pcf.InstanceIP != testCase.ExpectedVendorsHash.PCF.InstanceIP { t.Fatalf("%s: InstanceIP incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.PCF.InstanceIP, pcf.InstanceIP) } if pcf.MemoryLimit != testCase.ExpectedVendorsHash.PCF.MemoryLimit { t.Fatalf("%s: MemoryLimit incorrect; expected: %s; got: %s", testCase.TestName, testCase.ExpectedVendorsHash.PCF.MemoryLimit, pcf.MemoryLimit) } } } } go-agent-3.42.0/v3/internal/utilization/provider.go000066400000000000000000000027701510742411500222070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "fmt" "strings" "time" ) // Helper constants, functions, and types common to multiple providers are // contained in this file. // Constants from the spec. const ( maxFieldValueSize = 255 // The maximum value size, in bytes. providerTimeout = 500 * time.Millisecond // The maximum time a HTTP provider may block. lookupAddrTimeout = 500 * time.Millisecond ) type validationError struct{ e error } func (a validationError) Error() string { return a.e.Error() } func isValidationError(e error) bool { _, is := e.(validationError) return is } // This function normalises string values per the utilization spec. func normalizeValue(s string) (string, error) { out := strings.TrimSpace(s) bytes := []byte(out) if len(bytes) > maxFieldValueSize { return "", validationError{fmt.Errorf("response is too long: got %d; expected <=%d", len(bytes), maxFieldValueSize)} } for i, r := range out { if !isAcceptableRune(r) { return "", validationError{fmt.Errorf("bad character %x at position %d in response", r, i)} } } return out, nil } func isAcceptableRune(r rune) bool { switch r { case 0xFFFD: return false // invalid UTF-8 case '_', ' ', '/', '.', '-': return true default: return r > 0x7f || // still allows some invalid UTF-8, but that's the spec. ('0' <= r && r <= '9') || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') } } go-agent-3.42.0/v3/internal/utilization/provider_test.go000066400000000000000000000065261510742411500232510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "bytes" "encoding/json" "errors" "net/http" "testing" ) // Cross agent test types common to each provider's set of test cases. type testCase struct { TestName string `json:"testname"` URIs map[string]jsonResponse `json:"uri"` EnvVars map[string]envResponse `json:"env_vars"` ExpectedVendorsHash vendors `json:"expected_vendors_hash"` ExpectedMetrics map[string]metric `json:"expected_metrics"` } type envResponse struct { Response string `json:"response"` Timeout bool `json:"timeout"` } type jsonResponse struct { Response json.RawMessage `json:"response"` Timeout bool `json:"timeout"` } type metric struct { CallCount int `json:"call_count"` } var errTimeout = errors.New("timeout") type mockTransport struct { t *testing.T responses map[string]jsonResponse } type mockBody struct { bytes.Reader closed bool t *testing.T } func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) { // Half the requests are going to the test's endpoint, while the other half // are going to the AWS IMDSv2 token endpoint. Accept both. for match, response := range m.responses { if (r.URL.String() == match) || (r.URL.String() == awsTokenEndpoint) { return m.respond(response) } } m.t.Errorf("Unknown request URI: %s", r.URL.String()) return nil, nil } func (m *mockTransport) respond(resp jsonResponse) (*http.Response, error) { if resp.Timeout { return nil, errTimeout } return &http.Response{ Status: "200 OK", StatusCode: 200, Body: &mockBody{ t: m.t, Reader: *bytes.NewReader(resp.Response), }, }, nil } // This function is included simply so that http.Client doesn't complain. func (m *mockTransport) CancelRequest(r *http.Request) {} func (m *mockBody) Close() error { if m.closed { m.t.Error("Close of already closed connection!") } m.closed = true return nil } func (m *mockBody) ensureClosed() { if !m.closed { m.t.Error("Connection was not closed") } } func TestNormaliseValue(t *testing.T) { testCases := []struct { name string input string expected string isError bool }{ { name: "Valid - empty", input: "", expected: "", isError: false, }, { name: "Valid - symbols", input: ". /-_", expected: ". /-_", isError: false, }, { name: "Valid - string", input: "simplesentence", expected: "simplesentence", isError: false, }, { name: "Invalid - More than 255", input: `256256256256256256256256256256256256256256256256256256256256 256256256256256256256256256256256256256256256256256256256256256256256256 256256256256256256256256256256256256256256256256256256256256256256256256 2562562562562562562562562562562562562562562562562562`, expected: "", isError: true, }, } for _, tc := range testCases { actual, err := normalizeValue(tc.input) if tc.isError && err == nil { t.Fatalf("%s: expected error; got nil", tc.name) } else if !tc.isError { if err != nil { t.Fatalf("%s: expected not error; got: %v", tc.name, err) } if tc.expected != actual { t.Fatalf("%s: expected: %s; got: %s", tc.name, tc.expected, actual) } } } } go-agent-3.42.0/v3/internal/utilization/utilization.go000066400000000000000000000141001510742411500227160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package utilization implements the Utilization spec, available at // https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md package utilization import ( "net/http" "os" "runtime" "sync" "github.com/newrelic/go-agent/v3/internal/logger" "github.com/newrelic/go-agent/v3/internal/sysinfo" ) const ( metadataVersion = 5 ) // Config controls the behavior of utilization information capture. type Config struct { DetectAWS bool DetectAzure bool DetectGCP bool DetectPCF bool DetectDocker bool DetectKubernetes bool LogicalProcessors int TotalRAMMIB int BillingHostname string Hostname string } type override struct { LogicalProcessors *int `json:"logical_processors,omitempty"` TotalRAMMIB *int `json:"total_ram_mib,omitempty"` BillingHostname string `json:"hostname,omitempty"` } // Data contains utilization system information. type Data struct { MetadataVersion int `json:"metadata_version"` // Although `runtime.NumCPU()` will never fail, this field is a pointer // to facilitate the cross agent tests. LogicalProcessors *int `json:"logical_processors"` RAMMiB *uint64 `json:"total_ram_mib"` Hostname string `json:"hostname"` FullHostname string `json:"full_hostname,omitempty"` Addresses []string `json:"ip_address,omitempty"` BootID string `json:"boot_id,omitempty"` Config *override `json:"config,omitempty"` Vendors *vendors `json:"vendors,omitempty"` } var ( sampleRAMMib = uint64(1024) sampleLogicProc = int(16) // SampleData contains sample utilization data useful for testing. SampleData = Data{ MetadataVersion: metadataVersion, LogicalProcessors: &sampleLogicProc, RAMMiB: &sampleRAMMib, Hostname: "my-hostname", } ) type docker struct { ID string `json:"id,omitempty"` } type kubernetes struct { Host string `json:"kubernetes_service_host"` } type vendors struct { AWS *aws `json:"aws,omitempty"` Azure *azure `json:"azure,omitempty"` GCP *gcp `json:"gcp,omitempty"` PCF *pcf `json:"pcf,omitempty"` Docker *docker `json:"docker,omitempty"` Kubernetes *kubernetes `json:"kubernetes,omitempty"` } func (v *vendors) AnySet() bool { return v.AWS != nil || v.Azure != nil || v.GCP != nil || v.PCF != nil || v.Docker != nil || v.Kubernetes != nil } func (v *vendors) isEmpty() bool { return nil == v || *v == vendors{} } func overrideFromConfig(config Config) *override { ov := &override{} if 0 != config.LogicalProcessors { x := config.LogicalProcessors ov.LogicalProcessors = &x } if 0 != config.TotalRAMMIB { x := config.TotalRAMMIB ov.TotalRAMMIB = &x } ov.BillingHostname = config.BillingHostname if "" == ov.BillingHostname && nil == ov.LogicalProcessors && nil == ov.TotalRAMMIB { ov = nil } return ov } // Gather gathers system utilization data. func Gather(config Config, lg logger.Logger) *Data { client := &http.Client{ Timeout: providerTimeout, } return gatherWithClient(config, lg, client) } func gatherWithClient(config Config, lg logger.Logger, client *http.Client) *Data { var wg sync.WaitGroup cpu := runtime.NumCPU() uDat := &Data{ MetadataVersion: metadataVersion, LogicalProcessors: &cpu, Vendors: &vendors{}, } warnGatherError := func(datatype string, err error) { lg.Debug("error gathering utilization data", map[string]interface{}{ "error": err.Error(), "datatype": datatype, }) } // Gather IPs before spawning goroutines since the IPs are used in // gathering full hostname. if ips, err := utilizationIPs(); nil == err { uDat.Addresses = ips } else { warnGatherError("addresses", err) } // This closure allows us to run each gather function in a separate goroutine // and wait for them at the end by closing over the wg WaitGroup we // instantiated at the start of the function. goGather := func(datatype string, gather func(*Data, *http.Client) error) { wg.Add(1) go func() { // Note that locking around util is not necessary since // WaitGroup provides acts as a memory barrier: // https://groups.google.com/d/msg/golang-nuts/5oHzhzXCcmM/utEwIAApCQAJ // Thus this code is fine as long as each routine is // modifying a different field of util. defer wg.Done() if err := gather(uDat, client); err != nil { warnGatherError(datatype, err) } }() } // Kick off gathering which requires network calls in goroutines. if config.DetectAWS { goGather("aws", gatherAWS) } if config.DetectAzure { goGather("azure", gatherAzure) } if config.DetectPCF { goGather("pcf", gatherPCF) } if config.DetectGCP { goGather("gcp", gatherGCP) } wg.Add(1) go func() { defer wg.Done() uDat.FullHostname = getFQDN(uDat.Addresses) }() // Do non-network gathering sequentially since it is fast. if id, err := sysinfo.BootID(); err != nil { if err != sysinfo.ErrFeatureUnsupported { warnGatherError("bootid", err) } } else { uDat.BootID = id } if config.DetectKubernetes { gatherKubernetes(uDat.Vendors, os.Getenv) } if config.DetectDocker { if id, err := sysinfo.DockerID(); err != nil { if err != sysinfo.ErrFeatureUnsupported && err != sysinfo.ErrDockerNotFound { warnGatherError("docker", err) } } else { uDat.Vendors.Docker = &docker{ID: id} } } uDat.Hostname = config.Hostname if bts, err := sysinfo.PhysicalMemoryBytes(); nil == err { mib := sysinfo.BytesToMebibytes(bts) uDat.RAMMiB = &mib } else { warnGatherError("memory", err) } // Now we wait for everything! wg.Wait() // Override whatever needs to be overridden. uDat.Config = overrideFromConfig(config) if uDat.Vendors.isEmpty() { // Per spec, we MUST NOT send any vendors hash if it's empty. uDat.Vendors = nil } return uDat } func gatherKubernetes(v *vendors, getenv func(string) string) { if host := getenv("KUBERNETES_SERVICE_HOST"); host != "" { v.Kubernetes = &kubernetes{Host: host} } } go-agent-3.42.0/v3/internal/utilization/utilization_test.go000066400000000000000000000214361510742411500237670ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package utilization import ( "bytes" "encoding/json" "errors" "net/http" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" "github.com/newrelic/go-agent/v3/internal/logger" ) func TestJSONMarshalling(t *testing.T) { ramInitializer := new(uint64) *ramInitializer = 1024 actualProcessors := 4 configProcessors := 16 u := Data{ MetadataVersion: metadataVersion, LogicalProcessors: &actualProcessors, RAMMiB: ramInitializer, Hostname: "localhost", Vendors: &vendors{ AWS: &aws{ InstanceID: "8BADFOOD", InstanceType: "t2.micro", AvailabilityZone: "us-west-1", }, Docker: &docker{ID: "47cbd16b77c50cbf71401"}, Kubernetes: &kubernetes{Host: "10.96.0.1"}, }, Config: &override{ LogicalProcessors: &configProcessors, }, } expect := `{ "metadata_version": 5, "logical_processors": 4, "total_ram_mib": 1024, "hostname": "localhost", "config": { "logical_processors": 16 }, "vendors": { "aws": { "instanceId": "8BADFOOD", "instanceType": "t2.micro", "availabilityZone": "us-west-1" }, "docker": { "id": "47cbd16b77c50cbf71401" }, "kubernetes": { "kubernetes_service_host": "10.96.0.1" } } }` j, err := json.MarshalIndent(u, "", "\t") if err != nil { t.Error(err) } if string(j) != expect { t.Errorf("strings don't match; \nexpected: %s\n actual: %s\n", expect, string(j)) } // Test that we marshal not-present values to nil. u.RAMMiB = nil u.Hostname = "" u.Config = nil expect = `{ "metadata_version": 5, "logical_processors": 4, "total_ram_mib": null, "hostname": "", "vendors": { "aws": { "instanceId": "8BADFOOD", "instanceType": "t2.micro", "availabilityZone": "us-west-1" }, "docker": { "id": "47cbd16b77c50cbf71401" }, "kubernetes": { "kubernetes_service_host": "10.96.0.1" } } }` j, err = json.MarshalIndent(u, "", "\t") if err != nil { t.Error(err) } if string(j) != expect { t.Errorf("strings don't match; \nexpected: %s\n actual: %s\n", expect, string(j)) } } type errorRoundTripper struct{ error } func (e errorRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { return nil, e } // Smoke test the Gather method. func TestUtilizationHash(t *testing.T) { config := Config{ DetectAWS: true, DetectAzure: true, DetectDocker: true, } client := &http.Client{ Transport: errorRoundTripper{errors.New("timed out")}, } data := gatherWithClient(config, logger.ShimLogger{}, client) if data.MetadataVersion == 0 || nil == data.LogicalProcessors || 0 == *data.LogicalProcessors || data.RAMMiB == nil || *data.RAMMiB == 0 { t.Errorf("utilization data unexpected fields: %+v", data) } } func TestOverrideFromConfig(t *testing.T) { testcases := []struct { config Config expect string }{ {Config{}, `null`}, {Config{LogicalProcessors: 16}, `{"logical_processors":16}`}, {Config{TotalRAMMIB: 1024}, `{"total_ram_mib":1024}`}, {Config{BillingHostname: "localhost"}, `{"hostname":"localhost"}`}, {Config{ LogicalProcessors: 16, TotalRAMMIB: 1024, BillingHostname: "localhost", }, `{"logical_processors":16,"total_ram_mib":1024,"hostname":"localhost"}`}, } for _, tc := range testcases { ov := overrideFromConfig(tc.config) js, err := json.Marshal(ov) if nil != err { t.Error(tc.expect, err) continue } if string(js) != tc.expect { t.Error(tc.expect, string(js)) } } } type utilizationCrossAgentTestcase struct { Name string `json:"testname"` RAMMIB *uint64 `json:"input_total_ram_mib"` LogicalProcessors *int `json:"input_logical_processors"` Hostname string `json:"input_hostname"` FullHostname string `json:"input_full_hostname"` Addresses []string `json:"input_ip_address"` BootID string `json:"input_boot_id"` AWSID string `json:"input_aws_id"` AWSType string `json:"input_aws_type"` AWSZone string `json:"input_aws_zone"` AzureLocation string `json:"input_azure_location"` AzureName string `json:"input_azure_name"` AzureID string `json:"input_azure_id"` AzureSize string `json:"input_azure_size"` GCPID json.Number `json:"input_gcp_id"` GCPType string `json:"input_gcp_type"` GCPName string `json:"input_gcp_name"` GCPZone string `json:"input_gcp_zone"` PCFGUID string `json:"input_pcf_guid"` PCFIP string `json:"input_pcf_ip"` PCFMemLimit string `json:"input_pcf_mem_limit"` ExpectedOutput json.RawMessage `json:"expected_output_json"` Config struct { LogicalProcessors json.RawMessage `json:"NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS"` RAWMMIB json.RawMessage `json:"NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB"` Hostname string `json:"NEW_RELIC_UTILIZATION_BILLING_HOSTNAME"` KubernetesHost string `json:"KUBERNETES_SERVICE_HOST"` } `json:"input_environment_variables"` } func crossAgentVendors(tc utilizationCrossAgentTestcase) *vendors { v := &vendors{} if tc.AWSID != "" && tc.AWSType != "" && tc.AWSZone != "" { v.AWS = &aws{ InstanceID: tc.AWSID, InstanceType: tc.AWSType, AvailabilityZone: tc.AWSZone, } v.AWS.validate() } if tc.AzureLocation != "" && tc.AzureName != "" && tc.AzureID != "" && tc.AzureSize != "" { v.Azure = &azure{ Location: tc.AzureLocation, Name: tc.AzureName, VMID: tc.AzureID, VMSize: tc.AzureSize, } v.Azure.validate() } if tc.GCPID.String() != "" && tc.GCPType != "" && tc.GCPName != "" && tc.GCPZone != "" { v.GCP = &gcp{ ID: numericString(tc.GCPID.String()), MachineType: tc.GCPType, Name: tc.GCPName, Zone: tc.GCPZone, } v.GCP.validate() } if tc.PCFIP != "" && tc.PCFGUID != "" && tc.PCFMemLimit != "" { v.PCF = &pcf{ InstanceGUID: tc.PCFGUID, InstanceIP: tc.PCFIP, MemoryLimit: tc.PCFMemLimit, } v.PCF.validate() } gatherKubernetes(v, func(key string) string { if key == "KUBERNETES_SERVICE_HOST" { return tc.Config.KubernetesHost } return "" }) if v.isEmpty() { return nil } return v } func compactJSON(js []byte) []byte { buf := new(bytes.Buffer) if err := json.Compact(buf, js); err != nil { return nil } return buf.Bytes() } func runUtilizationCrossAgentTestcase(t *testing.T, tc utilizationCrossAgentTestcase) { var ConfigRAWMMIB int if nil != tc.Config.RAWMMIB { json.Unmarshal(tc.Config.RAWMMIB, &ConfigRAWMMIB) } var ConfigLogicalProcessors int if nil != tc.Config.LogicalProcessors { json.Unmarshal(tc.Config.LogicalProcessors, &ConfigLogicalProcessors) } cfg := Config{ LogicalProcessors: ConfigLogicalProcessors, TotalRAMMIB: ConfigRAWMMIB, BillingHostname: tc.Config.Hostname, } data := &Data{ MetadataVersion: metadataVersion, LogicalProcessors: tc.LogicalProcessors, RAMMiB: tc.RAMMIB, Hostname: tc.Hostname, BootID: tc.BootID, Vendors: crossAgentVendors(tc), Config: overrideFromConfig(cfg), FullHostname: tc.FullHostname, Addresses: tc.Addresses, } js, err := json.Marshal(data) if nil != err { t.Error(tc.Name, err) } expect := string(compactJSON(tc.ExpectedOutput)) if string(js) != expect { t.Error(tc.Name, string(js), expect) } } func TestUtilizationCrossAgent(t *testing.T) { var tcs []utilizationCrossAgentTestcase input, err := crossagent.ReadFile(`utilization/utilization_json.json`) if nil != err { t.Fatal(err) } err = json.Unmarshal(input, &tcs) if nil != err { t.Fatal(err) } for _, tc := range tcs { runUtilizationCrossAgentTestcase(t, tc) } } func TestVendorsIsEmpty(t *testing.T) { v := &vendors{} if !v.isEmpty() { t.Fatal("default vendors does not register as empty") } v.AWS = &aws{} v.Azure = &azure{} v.PCF = &pcf{} v.GCP = &gcp{} if v.isEmpty() { t.Fatal("non-empty vendors registers as empty") } var nilVendors *vendors if !nilVendors.isEmpty() { t.Fatal("nil vendors should be empty") } } func TestVendorsAnySet(t *testing.T) { v := &vendors{AWS: &aws{}, Azure: &azure{}, GCP: &gcp{}} expected := v.AnySet() if expected == false { t.Error("expected anyset to be true") } } func TestGather(t *testing.T) { config := Config{ DetectAWS: true, DetectAzure: true, DetectGCP: true, DetectPCF: true, DetectDocker: true, DetectKubernetes: true, Hostname: "test-hostname", } data := Gather(config, logger.ShimLogger{}) if data == nil { t.Error("Data expected to be non-nil") } } go-agent-3.42.0/v3/newrelic/000077500000000000000000000000001510742411500154415ustar00rootroot00000000000000go-agent-3.42.0/v3/newrelic/LICENSE.txt000066400000000000000000000264501510742411500172730ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Versions 3.8.0 and above for this project are licensed under Apache 2.0. For prior versions of this project, please see the LICENCE.txt file in the root directory of that version for more information. go-agent-3.42.0/v3/newrelic/adaptive_sampler.go000066400000000000000000000050541510742411500213140ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "math" "sync" "time" ) type adaptiveSampler struct { sync.Mutex period time.Duration target uint64 // Transactions with priority higher than this are sampled. // This is 1 - sampleRatio. priorityMin float32 currentPeriod struct { numSampled uint64 numSeen uint64 end time.Time } } // newAdaptiveSampler creates an adaptiveSampler. func newAdaptiveSampler(period time.Duration, target uint64, now time.Time) *adaptiveSampler { as := &adaptiveSampler{} as.period = period as.target = target as.currentPeriod.end = now.Add(period) // Sample the first transactions in the first period. as.priorityMin = 0.0 return as } // computeSampled calculates if the transaction should be sampled. func (as *adaptiveSampler) computeSampled(priority float32, now time.Time) bool { as.Lock() defer as.Unlock() // Never sample anything if the target is zero. This is not an expected // connect reply response, but it is used for the placeholder run (app // not connected yet), and is used for testing. if 0 == as.target { return false } // If the current time is after the end of the "currentPeriod". This is in // a `for`/`while` loop in case there's a harvest where no sampling happened. // i.e. for situations where a single call to // as.currentPeriod.end = as.currentPeriod.end.Add(as.period) // might not catch us up to the current period for now.After(as.currentPeriod.end) { as.priorityMin = 0.0 if as.currentPeriod.numSeen > 0 { sampledRatio := float32(as.target) / float32(as.currentPeriod.numSeen) as.priorityMin = 1.0 - sampledRatio } as.currentPeriod.numSampled = 0 as.currentPeriod.numSeen = 0 as.currentPeriod.end = as.currentPeriod.end.Add(as.period) } as.currentPeriod.numSeen++ // exponential backoff -- if the number of sampled items is greater than our // target, we need to apply the exponential backoff if as.currentPeriod.numSampled > as.target { if as.computeSampledBackoff(as.target, as.currentPeriod.numSeen, as.currentPeriod.numSampled) { as.currentPeriod.numSampled++ return true } return false } if priority >= as.priorityMin { as.currentPeriod.numSampled++ return true } return false } func (as *adaptiveSampler) computeSampledBackoff(target uint64, decidedCount uint64, sampledTrueCount uint64) bool { return float64(randUint64N(decidedCount)) < math.Pow(float64(target), (float64(target)/float64(sampledTrueCount)))-math.Pow(float64(target), 0.5) } go-agent-3.42.0/v3/newrelic/adaptive_sampler_test.go000066400000000000000000000061201510742411500223460ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" "time" ) func assert(t testing.TB, expectTrue bool) { if h, ok := t.(interface { Helper() }); ok { h.Helper() } if !expectTrue { t.Error(expectTrue) } } func TestAdaptiveSampler(t *testing.T) { start := time.Now() sampler := newAdaptiveSampler(60*time.Second, 2, start) // first period -- we're guaranteed to get 2 sampled // due to our target, and we'll send through a total of 4 assert(t, sampler.computeSampled(0.0, start)) assert(t, sampler.computeSampled(0.0, start)) sampler.computeSampled(0.0, start) sampler.computeSampled(0.0, start) // Next period! 4 calls in the last period means a new sample ratio // of 1/2. Nothing with a priority less than the ratio will get through now := start.Add(61 * time.Second) assert(t, !sampler.computeSampled(0.0, now)) assert(t, !sampler.computeSampled(0.0, now)) assert(t, !sampler.computeSampled(0.0, now)) assert(t, !sampler.computeSampled(0.0, now)) assert(t, !sampler.computeSampled(0.49, now)) assert(t, !sampler.computeSampled(0.49, now)) // but these two will get through, and we'll still be under // our target rate so there's no random sampling to deal with assert(t, sampler.computeSampled(0.55, now)) assert(t, sampler.computeSampled(1.0, now)) // Next period! 8 calls in the last period means a new sample ratio // of 1/4. now = start.Add(121 * time.Second) assert(t, !sampler.computeSampled(0.0, now)) assert(t, !sampler.computeSampled(0.5, now)) assert(t, !sampler.computeSampled(0.7, now)) assert(t, sampler.computeSampled(0.8, now)) } func TestAdaptiveSamplerSkipPeriod(t *testing.T) { start := time.Now() sampler := newAdaptiveSampler(60*time.Second, 2, start) // same as the previous test, we know we can get two through // and we'll send a total of 4 through assert(t, sampler.computeSampled(0.0, start)) assert(t, sampler.computeSampled(0.0, start)) sampler.computeSampled(0.0, start) sampler.computeSampled(0.0, start) // Two periods later! Since there was a period with no samples, priorityMin // should be zero now := start.Add(121 * time.Second) assert(t, sampler.computeSampled(0.0, now)) assert(t, sampler.computeSampled(0.0, now)) } func TestAdaptiveSamplerTarget(t *testing.T) { var target uint64 target = 20 start := time.Now() sampler := newAdaptiveSampler(60*time.Second, target, start) // we should always sample up to the number of target events for i := 0; uint64(i) < target; i++ { assert(t, sampler.computeSampled(0.0, start)) } // but now further calls to ComputeSampled are subject to exponential backoff. // this means their sampling is subject to a bit of randomness and we have no // guarantee of a true or false sample, just an increasing unlikeliness that // things will be sampled } func TestAdaptiveSamplerTargetZero(t *testing.T) { var target uint64 target = 0 start := time.Now() sampler := newAdaptiveSampler(60*time.Second, target, start) for i := 0; uint64(i) < 100; i++ { assert(t, !sampler.computeSampled(0.0, start)) } } go-agent-3.42.0/v3/newrelic/analytics_events.go000066400000000000000000000075431510742411500213540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "container/heap" "github.com/newrelic/go-agent/v3/internal/jsonx" ) type analyticsEvent struct { priority priority jsonWriter } type analyticsEventHeap []analyticsEvent type analyticsEvents struct { numSeen int failedHarvests int events analyticsEventHeap } func (events *analyticsEvents) NumSeen() float64 { return float64(events.numSeen) } func (events *analyticsEvents) NumSaved() float64 { return float64(len(events.events)) } func (h analyticsEventHeap) Len() int { return len(h) } func (h analyticsEventHeap) Less(i, j int) bool { return h[i].priority.isLowerPriority(h[j].priority) } func (h analyticsEventHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } // Push and Pop are unused: only heap.Init and heap.Fix are used. func (h analyticsEventHeap) Push(x interface{}) {} func (h analyticsEventHeap) Pop() interface{} { return nil } func newAnalyticsEvents(max int) *analyticsEvents { return &analyticsEvents{ numSeen: 0, events: make(analyticsEventHeap, 0, max), failedHarvests: 0, } } func (events *analyticsEvents) capacity() int { return cap(events.events) } func (events *analyticsEvents) addEvent(e analyticsEvent) { events.numSeen++ if events.capacity() == 0 { // Configurable event harvest limits may be zero. return } if len(events.events) < cap(events.events) { events.events = append(events.events, e) if len(events.events) == cap(events.events) { // Delay heap initialization so that we can have // deterministic ordering for integration tests (the max // is not being reached). heap.Init(events.events) } return } if e.priority.isLowerPriority((events.events)[0].priority) { return } events.events[0] = e heap.Fix(events.events, 0) } func (events *analyticsEvents) mergeFailed(other *analyticsEvents) { fails := other.failedHarvests + 1 if fails >= failedEventsAttemptsLimit { return } events.failedHarvests = fails events.Merge(other) } func (events *analyticsEvents) Merge(other *analyticsEvents) { allSeen := events.numSeen + other.numSeen for _, e := range other.events { events.addEvent(e) } events.numSeen = allSeen } func (events *analyticsEvents) CollectorJSON(agentRunID string) ([]byte, error) { if 0 == len(events.events) { return nil, nil } estimate := 256 * len(events.events) buf := bytes.NewBuffer(make([]byte, 0, estimate)) buf.WriteByte('[') jsonx.AppendString(buf, agentRunID) buf.WriteByte(',') buf.WriteByte('{') buf.WriteString(`"reservoir_size":`) jsonx.AppendUint(buf, uint64(cap(events.events))) buf.WriteByte(',') buf.WriteString(`"events_seen":`) jsonx.AppendUint(buf, uint64(events.numSeen)) buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('[') for i, e := range events.events { if i > 0 { buf.WriteByte(',') } e.WriteJSON(buf) } buf.WriteByte(']') buf.WriteByte(']') return buf.Bytes(), nil } // split splits the events into two. NOTE! The two event pools are not valid // priority queues, and should only be used to create JSON, not for adding any // events. func (events *analyticsEvents) split() (*analyticsEvents, *analyticsEvents) { // numSeen is conserved: e1.numSeen + e2.numSeen == events.numSeen. e1 := &analyticsEvents{ numSeen: len(events.events) / 2, events: make([]analyticsEvent, len(events.events)/2), failedHarvests: events.failedHarvests, } e2 := &analyticsEvents{ numSeen: events.numSeen - e1.numSeen, events: make([]analyticsEvent, len(events.events)-len(e1.events)), failedHarvests: events.failedHarvests, } // Note that slicing is not used to ensure that length == capacity for // e1.events and e2.events. copy(e1.events, events.events) copy(e2.events, events.events[len(events.events)/2:]) return e1, e2 } go-agent-3.42.0/v3/newrelic/analytics_events_test.go000066400000000000000000000204171510742411500224060ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "strconv" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) var ( agentRunID = `12345` ) type priorityWriter priority func (x priorityWriter) WriteJSON(buf *bytes.Buffer) { buf.WriteString(strconv.FormatFloat(float64(x), 'f', -1, 32)) } func sampleAnalyticsEvent(priority priority) analyticsEvent { return analyticsEvent{ priority, priorityWriter(priority), } } func TestBasic(t *testing.T) { events := newAnalyticsEvents(10) events.addEvent(sampleAnalyticsEvent(0.5)) events.addEvent(sampleAnalyticsEvent(0.5)) events.addEvent(sampleAnalyticsEvent(0.5)) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } expected := `["12345",{"reservoir_size":10,"events_seen":3},[0.5,0.5,0.5]]` if string(json) != expected { t.Error(string(json), expected) } if 3 != events.numSeen { t.Error(events.numSeen) } if 3 != events.NumSaved() { t.Error(events.NumSaved()) } } func TestEmpty(t *testing.T) { events := newAnalyticsEvents(10) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if nil != json { t.Error(string(json)) } if 0 != events.numSeen { t.Error(events.numSeen) } if 0 != events.NumSaved() { t.Error(events.NumSaved()) } } func TestSampling(t *testing.T) { events := newAnalyticsEvents(3) events.addEvent(sampleAnalyticsEvent(0.999999)) events.addEvent(sampleAnalyticsEvent(0.1)) events.addEvent(sampleAnalyticsEvent(0.9)) events.addEvent(sampleAnalyticsEvent(0.2)) events.addEvent(sampleAnalyticsEvent(0.8)) events.addEvent(sampleAnalyticsEvent(0.3)) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if string(json) != `["12345",{"reservoir_size":3,"events_seen":6},[0.8,0.999999,0.9]]` { t.Error(string(json)) } if 6 != events.numSeen { t.Error(events.numSeen) } if 3 != events.NumSaved() { t.Error(events.NumSaved()) } } func TestMergeEmpty(t *testing.T) { e1 := newAnalyticsEvents(10) e2 := newAnalyticsEvents(10) e1.Merge(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if nil != json { t.Error(string(json)) } if 0 != e1.numSeen { t.Error(e1.numSeen) } if 0 != e1.NumSaved() { t.Error(e1.NumSaved()) } } func TestMergeFull(t *testing.T) { e1 := newAnalyticsEvents(2) e2 := newAnalyticsEvents(3) e1.addEvent(sampleAnalyticsEvent(0.1)) e1.addEvent(sampleAnalyticsEvent(0.15)) e1.addEvent(sampleAnalyticsEvent(0.25)) e2.addEvent(sampleAnalyticsEvent(0.06)) e2.addEvent(sampleAnalyticsEvent(0.12)) e2.addEvent(sampleAnalyticsEvent(0.18)) e2.addEvent(sampleAnalyticsEvent(0.24)) e1.Merge(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if string(json) != `["12345",{"reservoir_size":2,"events_seen":7},[0.24,0.25]]` { t.Error(string(json)) } if 7 != e1.numSeen { t.Error(e1.numSeen) } if 2 != e1.NumSaved() { t.Error(e1.NumSaved()) } } func TestAnalyticsEventMergeFailedSuccess(t *testing.T) { e1 := newAnalyticsEvents(2) e2 := newAnalyticsEvents(3) e1.addEvent(sampleAnalyticsEvent(0.1)) e1.addEvent(sampleAnalyticsEvent(0.15)) e1.addEvent(sampleAnalyticsEvent(0.25)) e2.addEvent(sampleAnalyticsEvent(0.06)) e2.addEvent(sampleAnalyticsEvent(0.12)) e2.addEvent(sampleAnalyticsEvent(0.18)) e2.addEvent(sampleAnalyticsEvent(0.24)) e1.mergeFailed(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if string(json) != `["12345",{"reservoir_size":2,"events_seen":7},[0.24,0.25]]` { t.Error(string(json)) } if 7 != e1.numSeen { t.Error(e1.numSeen) } if 2 != e1.NumSaved() { t.Error(e1.NumSaved()) } if 1 != e1.failedHarvests { t.Error(e1.failedHarvests) } } func TestAnalyticsEventMergeFailedLimitReached(t *testing.T) { e1 := newAnalyticsEvents(2) e2 := newAnalyticsEvents(3) e1.addEvent(sampleAnalyticsEvent(0.1)) e1.addEvent(sampleAnalyticsEvent(0.15)) e1.addEvent(sampleAnalyticsEvent(0.25)) e2.addEvent(sampleAnalyticsEvent(0.06)) e2.addEvent(sampleAnalyticsEvent(0.12)) e2.addEvent(sampleAnalyticsEvent(0.18)) e2.addEvent(sampleAnalyticsEvent(0.24)) e2.failedHarvests = failedEventsAttemptsLimit e1.mergeFailed(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if string(json) != `["12345",{"reservoir_size":2,"events_seen":3},[0.15,0.25]]` { t.Error(string(json)) } if 3 != e1.numSeen { t.Error(e1.numSeen) } if 2 != e1.NumSaved() { t.Error(e1.NumSaved()) } if 0 != e1.failedHarvests { t.Error(e1.failedHarvests) } } func analyticsEventBenchmarkHelper(b *testing.B, w jsonWriter) { events := newAnalyticsEvents(internal.MaxTxnEvents) event := analyticsEvent{0, w} for n := 0; n < internal.MaxTxnEvents; n++ { events.addEvent(event) } b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { js, err := events.CollectorJSON(agentRunID) if nil != err { b.Fatal(err, js) } } } func BenchmarkTxnEventsCollectorJSON(b *testing.B) { event := &txnEvent{ FinalName: "WebTransaction/Go/zip/zap", Start: time.Now(), Duration: 2 * time.Second, Queuing: 1 * time.Second, Zone: apdexSatisfying, Attrs: nil, } analyticsEventBenchmarkHelper(b, event) } func BenchmarkCustomEventsCollectorJSON(b *testing.B) { now := time.Now() ce, err := createCustomEvent("myEventType", map[string]interface{}{ "string": "myString", "bool": true, "int64": int64(123), }, now) if nil != err { b.Fatal(err) } analyticsEventBenchmarkHelper(b, ce) } func BenchmarkErrorEventsCollectorJSON(b *testing.B) { e := txnErrorFromResponseCode(time.Now(), 503) e.Stack = getStackTrace() txnName := "WebTransaction/Go/zip/zap" event := &errorEvent{ errorData: e, txnEvent: txnEvent{ FinalName: txnName, Duration: 3 * time.Second, Attrs: nil, }, } analyticsEventBenchmarkHelper(b, event) } func TestSplitFull(t *testing.T) { events := newAnalyticsEvents(10) for i := 0; i < 15; i++ { events.addEvent(sampleAnalyticsEvent(priority(float32(i) / 10.0))) } // Test that the capacity cannot exceed the max. if 10 != events.capacity() { t.Error(events.capacity()) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } if string(j1) != `["12345",{"reservoir_size":5,"events_seen":5},[0.5,0.7,0.6,0.8,0.9]]` { t.Error(string(j1)) } if string(j2) != `["12345",{"reservoir_size":5,"events_seen":10},[1.1,1.4,1,1.3,1.2]]` { t.Error(string(j2)) } } func TestSplitNotFullOdd(t *testing.T) { events := newAnalyticsEvents(10) for i := 0; i < 7; i++ { events.addEvent(sampleAnalyticsEvent(priority(float32(i) / 10.0))) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } if string(j1) != `["12345",{"reservoir_size":3,"events_seen":3},[0,0.1,0.2]]` { t.Error(string(j1)) } if string(j2) != `["12345",{"reservoir_size":4,"events_seen":4},[0.3,0.4,0.5,0.6]]` { t.Error(string(j2)) } } func TestSplitNotFullEven(t *testing.T) { events := newAnalyticsEvents(10) for i := 0; i < 8; i++ { events.addEvent(sampleAnalyticsEvent(priority(float32(i) / 10.0))) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } if string(j1) != `["12345",{"reservoir_size":4,"events_seen":4},[0,0.1,0.2,0.3]]` { t.Error(string(j1)) } if string(j2) != `["12345",{"reservoir_size":4,"events_seen":4},[0.4,0.5,0.6,0.7]]` { t.Error(string(j2)) } } func TestAnalyticsEventsZeroCapacity(t *testing.T) { // Analytics events methods should be safe when configurable harvest // settings have an event limit of zero. events := newAnalyticsEvents(0) if 0 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } events.addEvent(sampleAnalyticsEvent(0.5)) if 1 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } js, err := events.CollectorJSON("agentRunID") if err != nil || js != nil { t.Error(err, string(js)) } } go-agent-3.42.0/v3/newrelic/apdex.go000066400000000000000000000022261510742411500170730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "time" // apdexZone is a transaction classification. type apdexZone int // https://en.wikipedia.org/wiki/Apdex const ( apdexNone apdexZone = iota apdexSatisfying apdexTolerating apdexFailing ) // apdexFailingThreshold calculates the threshold at which the transaction is // considered a failure. func apdexFailingThreshold(threshold time.Duration) time.Duration { return 4 * threshold } // calculateApdexZone calculates the apdex based on the transaction duration and // threshold. // // Note that this does not take into account whether or not the transaction // had an error. That is expected to be done by the caller. func calculateApdexZone(threshold, duration time.Duration) apdexZone { if duration <= threshold { return apdexSatisfying } if duration <= apdexFailingThreshold(threshold) { return apdexTolerating } return apdexFailing } func (zone apdexZone) label() string { switch zone { case apdexSatisfying: return "S" case apdexTolerating: return "T" case apdexFailing: return "F" default: return "" } } go-agent-3.42.0/v3/newrelic/apdex_test.go000066400000000000000000000020501510742411500201250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" "time" ) func dur(d int) time.Duration { return time.Duration(d) } func TestCalculateApdexZone(t *testing.T) { if z := calculateApdexZone(dur(10), dur(1)); z != apdexSatisfying { t.Fatal(z) } if z := calculateApdexZone(dur(10), dur(10)); z != apdexSatisfying { t.Fatal(z) } if z := calculateApdexZone(dur(10), dur(11)); z != apdexTolerating { t.Fatal(z) } if z := calculateApdexZone(dur(10), dur(40)); z != apdexTolerating { t.Fatal(z) } if z := calculateApdexZone(dur(10), dur(41)); z != apdexFailing { t.Fatal(z) } if z := calculateApdexZone(dur(10), dur(100)); z != apdexFailing { t.Fatal(z) } } func TestApdexLabel(t *testing.T) { if out := apdexSatisfying.label(); "S" != out { t.Fatal(out) } if out := apdexTolerating.label(); "T" != out { t.Fatal(out) } if out := apdexFailing.label(); "F" != out { t.Fatal(out) } if out := apdexNone.label(); "" != out { t.Fatal(out) } } go-agent-3.42.0/v3/newrelic/app_run.go000066400000000000000000000230671510742411500174440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "strings" "sync" "time" "github.com/newrelic/go-agent/v3/internal" ) // appRun contains information regarding a single connection session with the // collector. It is immutable after creation at application connect. type appRun struct { Reply *internal.ConnectReply // AttributeConfig is calculated on every connect since it depends on // the security policies. AttributeConfig *attributeConfig Config config // firstAppName is the value of Config.AppName up to the first semicolon. firstAppName string adaptiveSampler *adaptiveSampler // rulesCache caches the results of creating transaction names. It // exists here since it is specific to a set of rules and is shared // between transactions. rulesCache *rulesCache // harvestConfig contains configuration related to event limits and // flexible harvest periods. This field is created once at appRun // creation. harvestConfig harvestConfig // Error code caches for faster lookups O(1) ignoreErrorCodesCache map[int]bool expectErrorCodesCache map[int]bool mu sync.RWMutex } const ( txnNameCacheLimit = 40 ) func newAppRun(config config, reply *internal.ConnectReply) *appRun { run := &appRun{ Reply: reply, AttributeConfig: createAttributeConfig(config, reply.SecurityPolicies.AttributesInclude.Enabled()), Config: config, rulesCache: newRulesCache(txnNameCacheLimit), ignoreErrorCodesCache: make(map[int]bool), expectErrorCodesCache: make(map[int]bool), } // Overwrite local settings with any server-side-config settings // present. NOTE! This requires that the Config provided to this // function is a value and not a pointer: We do not want to change the // input Config with values particular to this connection. if v := run.Reply.ServerSideConfig.TransactionTracerEnabled; v != nil { run.Config.TransactionTracer.Enabled = *v } if v := run.Reply.ServerSideConfig.ErrorCollectorEnabled; v != nil { run.Config.ErrorCollector.Enabled = *v } if v := run.Reply.ServerSideConfig.CrossApplicationTracerEnabled; v != nil { run.Config.CrossApplicationTracer.Enabled = *v } if v := run.Reply.ServerSideConfig.TransactionTracerThreshold; v != nil { switch val := v.(type) { case float64: run.Config.TransactionTracer.Threshold.IsApdexFailing = false run.Config.TransactionTracer.Threshold.Duration = internal.FloatSecondsToDuration(val) case string: if val == "apdex_f" { run.Config.TransactionTracer.Threshold.IsApdexFailing = true } } } if v := run.Reply.ServerSideConfig.TransactionTracerStackTraceThreshold; v != nil { run.Config.TransactionTracer.Segments.StackTraceThreshold = internal.FloatSecondsToDuration(*v) } if v := run.Reply.ServerSideConfig.ErrorCollectorIgnoreStatusCodes; v != nil { run.Config.ErrorCollector.IgnoreStatusCodes = v } if run.Config.ErrorCollector.IgnoreStatusCodes != nil { run.mu.Lock() for _, errorCode := range run.Config.ErrorCollector.IgnoreStatusCodes { run.ignoreErrorCodesCache[errorCode] = true } run.mu.Unlock() } if v := run.Reply.ServerSideConfig.ErrorCollectorExpectStatusCodes; v != nil { run.Config.ErrorCollector.ExpectStatusCodes = v } if run.Config.ErrorCollector.ExpectStatusCodes != nil { run.mu.Lock() for _, errorCode := range run.Config.ErrorCollector.ExpectStatusCodes { run.expectErrorCodesCache[errorCode] = true } run.mu.Unlock() } if !run.Reply.CollectErrorEvents { run.Config.ErrorCollector.CaptureEvents = false } if !run.Reply.CollectAnalyticsEvents { run.Config.TransactionEvents.Enabled = false } if !run.Reply.CollectTraces { run.Config.TransactionTracer.Enabled = false run.Config.DatastoreTracer.SlowQuery.Enabled = false } if !run.Reply.CollectSpanEvents { run.Config.SpanEvents.Enabled = false } // Distributed tracing takes priority over cross-app-tracing per: // https://source.datanerd.us/agents/agent-specs/blob/master/Distributed-Tracing.md#distributed-trace-payload if run.Config.DistributedTracer.Enabled { run.Config.CrossApplicationTracer.Enabled = false } // Cache the first application name set on the config run.firstAppName = strings.SplitN(config.AppName, ";", 2)[0] run.adaptiveSampler = newAdaptiveSampler( time.Duration(reply.SamplingTargetPeriodInSeconds)*time.Second, reply.SamplingTarget, time.Now()) if run.Reply.RunID != "" { js, _ := json.Marshal(settings(run.Config.Config)) run.Config.Logger.Debug("final configuration", map[string]interface{}{ "config": jsonString(js), }) } run.harvestConfig = harvestConfig{ ReportPeriods: run.ReportPeriods(), MaxTxnEvents: run.limit(run.Config.TransactionEvents.MaxSamplesStored, run.ptrTxnEvents), MaxCustomEvents: run.limit(run.Config.CustomInsightsEvents.MaxSamplesStored, run.ptrCustomEvents), MaxErrorEvents: run.limit(run.Config.ErrorCollector.MaxSamplesStored, run.ptrErrorEvents), MaxSpanEvents: run.limit(run.Config.SpanEvents.MaxSamplesStored, run.ptrSpanEvents), LoggingConfig: run.LoggingConfig(), } return run } func newPlaceholderAppRun(config config) *appRun { reply := internal.ConnectReplyDefaults() // Do no sampling if the app isn't connected: reply.SamplingTarget = 0 return newAppRun(config, reply) } const ( // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md#distributed-tracing serverlessDefaultPrimaryAppID = "Unknown" ) func newServerlessConnectReply(config config) *internal.ConnectReply { reply := internal.ConnectReplyDefaults() reply.ApdexThresholdSeconds = config.ServerlessMode.ApdexThreshold.Seconds() reply.AccountID = config.ServerlessMode.AccountID reply.TrustedAccountKey = config.ServerlessMode.TrustedAccountKey reply.PrimaryAppID = config.ServerlessMode.PrimaryAppID if reply.TrustedAccountKey == "" { // The trust key does not need to be provided by customers whose // account ID is the same as the trust key. reply.TrustedAccountKey = reply.AccountID } if reply.PrimaryAppID == "" { reply.PrimaryAppID = serverlessDefaultPrimaryAppID } // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md#adaptive-sampling reply.SamplingTargetPeriodInSeconds = 60 reply.SamplingTarget = 10 return reply } func (run *appRun) responseCodeIsError(code int) bool { // Response codes below 100 are allowed to be errors to support gRPC. if code < 400 && code >= 100 { return false } run.mu.RLock() defer run.mu.RUnlock() return !run.ignoreErrorCodesCache[code] } func (run *appRun) responseCodeIsExpected(code int) bool { run.mu.RLock() defer run.mu.RUnlock() return run.expectErrorCodesCache[code] } func (run *appRun) txnTraceThreshold(apdexThreshold time.Duration) time.Duration { if run.Config.TransactionTracer.Threshold.IsApdexFailing { return apdexFailingThreshold(apdexThreshold) } return run.Config.TransactionTracer.Threshold.Duration } func (run *appRun) ptrTxnEvents() *uint { return run.Reply.EventData.Limits.TxnEvents } func (run *appRun) ptrCustomEvents() *uint { return run.Reply.EventData.Limits.CustomEvents } func (run *appRun) ptrLogEvents() *uint { return run.Reply.EventData.Limits.LogEvents } func (run *appRun) ptrErrorEvents() *uint { return run.Reply.EventData.Limits.ErrorEvents } func (run *appRun) ptrSpanEvents() *uint { return run.Reply.SpanEventHarvestConfig.HarvestLimit } func (run *appRun) LoggingConfig() (config loggingConfig) { logging := run.Config.ApplicationLogging config.loggingEnabled = logging.Enabled config.collectEvents = logging.Enabled && logging.Forwarding.Enabled && !run.Config.HighSecurity config.maxLogEvents = run.limit(logging.Forwarding.MaxSamplesStored, run.ptrLogEvents) config.collectMetrics = logging.Enabled && logging.Metrics.Enabled config.localEnrichment = logging.Enabled && logging.LocalDecorating.Enabled if run.Config.Labels != nil && logging.Forwarding.Enabled && logging.Forwarding.Labels.Enabled { config.includeLabels = run.Config.Labels config.excludeLabels = &logging.Forwarding.Labels.Exclude } if run.Config.CustomInsightsEvents.CustomAttributesValues != nil && logging.Forwarding.Enabled && run.Config.CustomInsightsEvents.CustomAttributesEnabled { config.customAttributes = run.Config.CustomInsightsEvents.CustomAttributesValues } return config } func (run *appRun) limit(configMaxSamplesStored int, field func() *uint) int { if field() != nil { return int(*field()) } return configMaxSamplesStored } func (run *appRun) ReportPeriods() map[harvestTypes]time.Duration { fixed := harvestMetricsTraces configurable := harvestTypes(0) for tp, fn := range map[harvestTypes]func() *uint{ harvestTxnEvents: run.ptrTxnEvents, harvestCustomEvents: run.ptrCustomEvents, harvestLogEvents: run.ptrLogEvents, harvestErrorEvents: run.ptrErrorEvents, harvestSpanEvents: run.ptrSpanEvents, } { if run != nil && fn() != nil { configurable |= tp } else { fixed |= tp } } return map[harvestTypes]time.Duration{ configurable: run.Reply.ConfigurablePeriod(), fixed: fixedHarvestPeriod, } } func (run *appRun) createTransactionName(input string, isWeb bool) string { if name := run.rulesCache.find(input, isWeb); name != "" { return name } name := internal.CreateFullTxnName(input, run.Reply, isWeb) if name != "" { // Note that we don't cache situations where the rules say // ignore. It would increase complication (we would need to // disambiguate not-found vs ignore). Also, the ignore code // path is probably extremely uncommon. run.rulesCache.set(input, isWeb, name) } return name } go-agent-3.42.0/v3/newrelic/app_run_test.go000066400000000000000000000554511510742411500205050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "reflect" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) func TestResponseCodeIsError(t *testing.T) { cfg := config{Config: defaultConfig()} cfg.ErrorCollector.IgnoreStatusCodes = append(cfg.ErrorCollector.IgnoreStatusCodes, 504) run := newAppRun(cfg, internal.ConnectReplyDefaults()) for _, tc := range []struct { Code int IsError bool }{ {Code: 0, IsError: false}, // gRPC {Code: 1, IsError: true}, // gRPC {Code: 5, IsError: false}, // gRPC {Code: 6, IsError: true}, // gRPC {Code: 99, IsError: true}, {Code: 100, IsError: false}, {Code: 199, IsError: false}, {Code: 200, IsError: false}, {Code: 300, IsError: false}, {Code: 399, IsError: false}, {Code: 400, IsError: true}, {Code: 404, IsError: false}, {Code: 503, IsError: true}, {Code: 504, IsError: false}, } { if is := run.responseCodeIsError(tc.Code); is != tc.IsError { t.Errorf("responseCodeIsError for %d, wanted=%v got=%v", tc.Code, tc.IsError, is) } } } func TestResponseCodeIsExpected(t *testing.T) { cfg := config{Config: defaultConfig()} cfg.ErrorCollector.ExpectStatusCodes = []int{400, 503, 504} run := newAppRun(cfg, internal.ConnectReplyDefaults()) for _, tc := range []struct { Code int IsError bool }{ {Code: 0, IsError: false}, // gRPC {Code: 1, IsError: false}, // gRPC {Code: 400, IsError: true}, {Code: 404, IsError: false}, {Code: 503, IsError: true}, {Code: 504, IsError: true}, } { if is := run.responseCodeIsExpected(tc.Code); is != tc.IsError { t.Errorf("responseCodeIsError for %d, wanted=%v got=%v", tc.Code, tc.IsError, is) } } } func BenchmarkResponseCodeIsExpectedHit(b *testing.B) { cfg := config{Config: defaultConfig()} cfg.ErrorCollector.ExpectStatusCodes = []int{400, 503, 504} run := newAppRun(cfg, internal.ConnectReplyDefaults()) b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { run.responseCodeIsExpected(400) } } func TestCrossAppTracingEnabled(t *testing.T) { // CAT should NOT be enabled by default. cfg := config{Config: defaultConfig()} run := newAppRun(cfg, internal.ConnectReplyDefaults()) if enabled := run.Config.CrossApplicationTracer.Enabled; enabled { t.Error(enabled) } // DT gets priority over CAT. cfg = config{Config: defaultConfig()} cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = true run = newAppRun(cfg, internal.ConnectReplyDefaults()) if enabled := run.Config.CrossApplicationTracer.Enabled; enabled { t.Error(enabled) } cfg = config{Config: defaultConfig()} cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = false run = newAppRun(cfg, internal.ConnectReplyDefaults()) if enabled := run.Config.CrossApplicationTracer.Enabled; enabled { t.Error(enabled) } cfg = config{Config: defaultConfig()} cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = true run = newAppRun(cfg, internal.ConnectReplyDefaults()) if enabled := run.Config.CrossApplicationTracer.Enabled; !enabled { t.Error(enabled) } } func TestTxnTraceThreshold(t *testing.T) { // Test that the default txn trace threshold is the failing apdex. cfg := config{Config: defaultConfig()} run := newAppRun(cfg, internal.ConnectReplyDefaults()) threshold := run.txnTraceThreshold(1 * time.Second) if threshold != 4*time.Second { t.Error(threshold) } // Test that the trace threshold can be assigned to a fixed value. cfg = config{Config: defaultConfig()} cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 3 * time.Second run = newAppRun(cfg, internal.ConnectReplyDefaults()) threshold = run.txnTraceThreshold(1 * time.Second) if threshold != 3*time.Second { t.Error(threshold) } // Test that the trace threshold can be overwritten by server-side-config. // with "apdex_f". cfg = config{Config: defaultConfig()} cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 3 * time.Second reply := internal.ConnectReplyDefaults() json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.transaction_threshold":"apdex_f"}}`), &reply) run = newAppRun(cfg, reply) threshold = run.txnTraceThreshold(1 * time.Second) if threshold != 4*time.Second { t.Error(threshold) } // Test that the trace threshold can be overwritten by server-side-config. // with a numberic value. cfg = config{Config: defaultConfig()} reply = internal.ConnectReplyDefaults() json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.transaction_threshold":3}}`), &reply) run = newAppRun(cfg, reply) threshold = run.txnTraceThreshold(1 * time.Second) if threshold != 3*time.Second { t.Error(threshold) } } func TestEmptyReplyEventHarvestDefaults(t *testing.T) { run := newAppRun(config{Config: defaultConfig()}, &internal.ConnectReply{}) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: internal.MaxTxnEvents, maxCustomEvents: internal.MaxCustomEvents, maxErrorEvents: internal.MaxErrorEvents, maxSpanEvents: run.Config.DistributedTracer.ReservoirLimit, maxLogEvents: internal.MaxLogEvents, periods: map[harvestTypes]time.Duration{ harvestTypesAll: 60 * time.Second, 0: 60 * time.Second, }, }) } func TestEventHarvestFieldsAllPopulated(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": 5000, "harvest_limits": { "analytic_event_data": 1, "custom_event_data": 2, "log_event_data": 3, "error_event_data": 4 } }, "span_event_harvest_config":{ "report_period_ms": 10000, "harvest_limit": 5 } }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: 1, maxCustomEvents: 2, maxLogEvents: 3, maxErrorEvents: 4, maxSpanEvents: 5, periods: map[harvestTypes]time.Duration{ harvestMetricsTraces: 60 * time.Second, harvestTypesEvents: 5 * time.Second, }, }) } func TestZeroReportPeriod(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": 0 } }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: internal.MaxTxnEvents, maxCustomEvents: internal.MaxCustomEvents, maxLogEvents: internal.MaxLogEvents, maxErrorEvents: internal.MaxErrorEvents, maxSpanEvents: internal.MaxSpanEvents, periods: map[harvestTypes]time.Duration{ harvestTypesAll: 60 * time.Second, 0: 60 * time.Second, }, }) } func TestConnectResponseOnlySpanEvents(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "span_event_harvest_config":{ "report_period_ms": 10000, "harvest_limit": 3 }}}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: internal.MaxTxnEvents, maxCustomEvents: internal.MaxCustomEvents, maxLogEvents: internal.MaxLogEvents, maxErrorEvents: internal.MaxErrorEvents, maxSpanEvents: 3, periods: map[harvestTypes]time.Duration{ harvestTypesAll ^ harvestSpanEvents: 60 * time.Second, 2: 60 * time.Second, }, }) } func TestEventHarvestFieldsOnlyTxnEvents(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": 5000, "harvest_limits": { "analytic_event_data": 3 } }}}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: 3, maxCustomEvents: internal.MaxCustomEvents, maxErrorEvents: internal.MaxErrorEvents, maxSpanEvents: run.Config.DistributedTracer.ReservoirLimit, maxLogEvents: internal.MaxLogEvents, periods: map[harvestTypes]time.Duration{ harvestTypesAll ^ harvestTxnEvents: 60 * time.Second, harvestTxnEvents: 5 * time.Second, }, }) } func TestEventHarvestFieldsOnlyErrorEvents(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": 5000, "harvest_limits": { "error_event_data": 3 } }}}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: internal.MaxTxnEvents, maxCustomEvents: internal.MaxCustomEvents, maxLogEvents: internal.MaxLogEvents, maxErrorEvents: 3, maxSpanEvents: run.Config.DistributedTracer.ReservoirLimit, periods: map[harvestTypes]time.Duration{ harvestTypesAll ^ harvestErrorEvents: 60 * time.Second, harvestErrorEvents: 5 * time.Second, }, }) } func TestEventHarvestFieldsOnlyCustomEvents(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": 5000, "harvest_limits": { "custom_event_data": 3 } }}}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) assertHarvestConfig(t, run.harvestConfig, expectHarvestConfig{ maxTxnEvents: internal.MaxTxnEvents, maxCustomEvents: 3, maxLogEvents: internal.MaxLogEvents, maxErrorEvents: internal.MaxErrorEvents, maxSpanEvents: run.Config.DistributedTracer.ReservoirLimit, periods: map[harvestTypes]time.Duration{ harvestTypesAll ^ harvestCustomEvents: 60 * time.Second, harvestCustomEvents: 5 * time.Second, }, }) } func TestConfigurableHarvestNegativeReportPeriod(t *testing.T) { h, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "event_harvest_config": { "report_period_ms": -1 }}}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } expect := time.Duration(internal.DefaultConfigurableEventHarvestMs) * time.Millisecond if period := h.ConfigurablePeriod(); period != expect { t.Fatal(expect, period) } } func TestReplyTraceIDGenerator(t *testing.T) { // Test that the default connect reply has a populated trace id // generator that works. reply := internal.ConnectReplyDefaults() id1 := reply.TraceIDGenerator.GenerateTraceID() id2 := reply.TraceIDGenerator.GenerateTraceID() if len(id1) != 32 || len(id2) != 32 || id1 == id2 { t.Error(id1, id2) } spanID1 := reply.TraceIDGenerator.GenerateSpanID() spanID2 := reply.TraceIDGenerator.GenerateSpanID() if len(spanID1) != 16 || len(spanID2) != 16 || spanID1 == spanID2 { t.Error(spanID1, spanID2) } } func TestConfigurableTxnEvents_withCollResponse(t *testing.T) { h, err := internal.UnmarshalConnectReply([]byte( `{"return_value":{ "event_harvest_config": { "report_period_ms": 10000, "harvest_limits": { "analytic_event_data": 15 } } }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, h) // changed this line because I believe we are not changing the local config based on the response but just the harvest config if run.harvestConfig.MaxTxnEvents != 15 { t.Errorf("Unexpected max number of txn events, expected %d but got %d", 15, run.harvestConfig.MaxTxnEvents) } } func TestConfigurableTxnEvents_configMoreThanMax(t *testing.T) { h, err := internal.UnmarshalConnectReply([]byte( `{"return_value":{ "event_harvest_config": { "report_period_ms": 10000 } }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } cfg := config{Config: defaultConfig()} // Original // cfg.TransactionEvents.MaxSamplesStored = internal.MaxTxnEvents + 100 // through code (almost like a setter) cfg.TransactionEvents.MaxSamplesStored = maxTxnEvents(internal.MaxTxnEvents + 100) run := newAppRun(cfg, h) if run.harvestConfig.MaxTxnEvents != internal.MaxTxnEvents { t.Errorf("Unexpected max number of txn events, expected %d but got %d", internal.MaxTxnEvents, run.harvestConfig.MaxTxnEvents) } } func TestConfigurableTxnEvents_notInCollResponse(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte( `{"return_value":{ "event_harvest_config": { "report_period_ms": 10000 } }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } expected := 10 cfg := config{Config: defaultConfig()} cfg.TransactionEvents.MaxSamplesStored = expected run := newAppRun(cfg, reply) if run.Config.TransactionEvents.MaxSamplesStored != expected { t.Errorf("Unexpected max number of txn events, expected %d but got %d", expected, run.Config.TransactionEvents.MaxSamplesStored) } } type expectHarvestConfig struct { maxTxnEvents int maxCustomEvents int maxErrorEvents int maxSpanEvents int maxLogEvents int periods map[harvestTypes]time.Duration } func errorExpectNotEqualActual(value string, actual, expect interface{}) error { return fmt.Errorf("Expected %s value does not match actual; actual: %+v expect: %+v", value, actual, expect) } func assertHarvestConfig(t testing.TB, hc harvestConfig, expect expectHarvestConfig) { if h, ok := t.(interface { Helper() }); ok { h.Helper() } if max := hc.MaxTxnEvents; max != expect.maxTxnEvents { t.Error(errorExpectNotEqualActual("maxTxnEvents", max, expect.maxTxnEvents)) } if max := hc.MaxCustomEvents; max != expect.maxCustomEvents { t.Error(errorExpectNotEqualActual("MaxCustomEvents", max, expect.maxCustomEvents)) } if max := hc.MaxSpanEvents; max != expect.maxSpanEvents { t.Error(errorExpectNotEqualActual("MaxSpanEvents", max, expect.maxSpanEvents)) } if max := hc.MaxErrorEvents; max != expect.maxErrorEvents { t.Error(errorExpectNotEqualActual("MaxErrorEvents", max, expect.maxErrorEvents)) } if max := hc.LoggingConfig.maxLogEvents; max != expect.maxLogEvents { t.Error(errorExpectNotEqualActual("MaxLogEvents", max, expect.maxErrorEvents)) } if periods := hc.ReportPeriods; !reflect.DeepEqual(periods, expect.periods) { t.Error(errorExpectNotEqualActual("ReportPeriods", periods, expect.periods)) } } func TestPlaceholderAppRunSampler(t *testing.T) { // Test that the placeholder run used before connect does not sample // transactions. run := newPlaceholderAppRun(config{Config: defaultConfig()}) if sampled := run.adaptiveSampler.computeSampled(1.0, time.Now()); sampled { t.Fatal(sampled) } } func TestAppRunSampler(t *testing.T) { // Test that a default app run samples transactions. // Test that the default txn trace threshold is the failing apdex. cfg := config{Config: defaultConfig()} run := newAppRun(cfg, internal.ConnectReplyDefaults()) if sampled := run.adaptiveSampler.computeSampled(1.0, time.Now()); !sampled { t.Fatal(sampled) } if run.adaptiveSampler.target != 10 || run.adaptiveSampler.period != 60*time.Second { t.Fatal("invalid sampler initialization", run.adaptiveSampler.target, run.adaptiveSampler.period) } } func TestCreateTransactionName(t *testing.T) { reply, err := internal.UnmarshalConnectReply([]byte(`{"return_value":{ "url_rules":[ {"match_expression":"zip","each_segment":true,"replacement":"zoop"} ], "transaction_name_rules":[ {"match_expression":"WebTransaction/Go/zap/zoop/zep", "replacement":"WebTransaction/Go/zap/zoop/zep/zup/zyp"} ], "transaction_segment_terms":[ {"prefix": "WebTransaction/Go/", "terms": ["zyp", "zoop", "zap"]} ] }}`), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) want := "WebTransaction/Go/zap/zoop/*/zyp" if out := run.createTransactionName("/zap/zip/zep", true); out != want { t.Error("wanted:", want, "got:", out) } // Check that the cache was populated as expected. if out := run.rulesCache.find("/zap/zip/zep", true); out != want { t.Error("wanted:", want, "got:", out) } // Check that the next call returns the same output. if out := run.createTransactionName("/zap/zip/zep", true); out != want { t.Error("wanted:", want, "got:", out) } } func testMockConnectReply(t *testing.T, retVal string) *internal.ConnectReply { h, err := internal.UnmarshalConnectReply([]byte(retVal), internal.PreconnectReply{}) if nil != err { t.Fatal(err) } return h } func uintPtr(v uint) *uint { return &v } func Test_appRun_limit(t *testing.T) { tests := []struct { name string configMaxSamplesStored int fieldValue *uint // nil means field() returns nil want int }{ { name: "field returns nil, use config value", configMaxSamplesStored: 1000, fieldValue: nil, want: 1000, }, { name: "field returns value, use field value", configMaxSamplesStored: 1000, fieldValue: uintPtr(500), want: 500, }, { name: "field returns zero, use field value", configMaxSamplesStored: 1000, fieldValue: uintPtr(0), want: 0, }, { name: "config is zero, field returns nil", configMaxSamplesStored: 0, fieldValue: nil, want: 0, }, { name: "config is zero, field returns value", configMaxSamplesStored: 0, fieldValue: uintPtr(100), want: 100, }, { name: "config is negative, field returns nil", // keeping this test so we know whatever value exists, we will use configMaxSamplesStored: -1, fieldValue: nil, want: -1, }, { name: "config is negative, field returns value", configMaxSamplesStored: -1, fieldValue: uintPtr(200), want: 200, }, { name: "field returns large value", configMaxSamplesStored: 1000, fieldValue: uintPtr(999999), want: 999999, }, { name: "field returns 1", configMaxSamplesStored: 1000, fieldValue: uintPtr(1), want: 1, }, { name: "config and field both large values", configMaxSamplesStored: 50000, fieldValue: uintPtr(60000), want: 60000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { run := &appRun{} // Create a field function that returns the test value fieldFunc := func() *uint { return tt.fieldValue } got := run.limit(tt.configMaxSamplesStored, fieldFunc) if got != tt.want { t.Errorf("limit() = %v, want %v", got, tt.want) } }) } } // Since we are using uint we are expecting a non-negative number in the harvester response. // If there were to be a negative number in the test case, it would cause an error when // unmarshaling the json response. This test in only testing how the response is handled // it does not matter what the limits are in this case func Test_appRun_ptrEventsMethods(t *testing.T) { type eventTypeTest struct { name string methodName string method func(*appRun) *uint jsonKey string configKey string } eventTypes := []eventTypeTest{ { name: "TxnEvents", methodName: "ptrTxnEvents", method: (*appRun).ptrTxnEvents, jsonKey: "event_harvest_config", configKey: `{"analytic_event_data": %s}`, }, { name: "CustomEvents", methodName: "ptrCustomEvents", method: (*appRun).ptrCustomEvents, jsonKey: "event_harvest_config", configKey: `{"custom_event_data": %s}`, }, { name: "LogEvents", methodName: "ptrLogEvents", method: (*appRun).ptrLogEvents, jsonKey: "event_harvest_config", configKey: `{"log_event_data": %s}`, }, { name: "ErrorEvents", methodName: "ptrErrorEvents", method: (*appRun).ptrErrorEvents, jsonKey: "event_harvest_config", configKey: `{"error_event_data": %s}`, }, { name: "SpanEvents", methodName: "ptrSpanEvents", method: (*appRun).ptrSpanEvents, jsonKey: "span_event_harvest_config", configKey: `%s`, }, } testCases := []struct { name string format string harvest_limit string want *uint }{ { name: "limit is set to 2000", format: `{"return_value": {"%s": {"%s": %s}}}`, harvest_limit: "2000", want: uintPtr(2000), }, { name: "limit is set to 0", format: `{"return_value": {"%s": {"%s": %s}}}`, harvest_limit: "0", want: uintPtr(0), }, { name: "limit is set to 1", format: `{"return_value": {"%s": {"%s": %s}}}`, harvest_limit: "1", want: uintPtr(1), }, { name: "limit is set to large value", format: `{"return_value": {"%s": {"%s": %s}}}`, harvest_limit: "999999", want: uintPtr(999999), }, { name: "config section is null", format: `{"return_value": {"%s": {"%s": null}}}`, harvest_limit: "null", want: nil, }, { name: "limit field is null", format: `{"return_value": {"%s": {"%s": %s}}}`, harvest_limit: "null", want: nil, }, { name: "config section is missing", format: `{"return_value": {}}`, harvest_limit: "null", want: nil, }, } for _, eventType := range eventTypes { t.Run(eventType.name, func(t *testing.T) { harvestLimitField := "harvest_limits" if eventType.name == "SpanEvents" { harvestLimitField = "harvest_limit" } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { var jsonStr string switch tt.name { case "config section is missing": jsonStr = tt.format case "config section is null": jsonStr = fmt.Sprintf(tt.format, harvestLimitField, eventType.jsonKey) default: harvestLimit := fmt.Sprintf(eventType.configKey, tt.harvest_limit) jsonStr = fmt.Sprintf(tt.format, eventType.jsonKey, harvestLimitField, harvestLimit) } reply := testMockConnectReply(t, jsonStr) run := &appRun{Reply: reply} got := eventType.method(run) if tt.want == nil { if got != nil { t.Errorf("%s() = %v, want nil", eventType.methodName, got) } } else { if got == nil { t.Errorf("%s() = nil, want %v", eventType.methodName, *tt.want) } else if *got != *tt.want { t.Errorf("%s() = %v, want %v", eventType.methodName, *got, *tt.want) } } }) } }) } } go-agent-3.42.0/v3/newrelic/application.go000066400000000000000000000244361510742411500203040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "os" "time" ) // Application represents your application. All methods on Application are nil // safe. Therefore, a nil Application pointer can be safely used as a mock. type Application struct { Private interface{} app *app } /* // IsAIMonitoringEnabled returns true if monitoring for the specified mode of the named integration is enabled. func (app *Application) IsAIMonitoringEnabled(integration string, streaming bool) bool { if app == nil || app.app == nil { return false } run, _ := app.app.getState() if run == nil { return false } aiconf := run.Config.AIMonitoring if !aiconf.Enabled { return false } if aiconf.IncludeOnly != nil && integration != "" && !slices.Contains(aiconf.IncludeOnly, integration) { return false } if streaming && !aiconf.Streaming { return false } return true } */ // GetLinkingMetadata returns the fields needed to link data to // an entity. This will return an empty struct if the application // is not connected or nil. func (app *Application) GetLinkingMetadata() LinkingMetadata { if app == nil || app.app == nil { return LinkingMetadata{} } reply, err := app.app.getState() if err != nil { app.app.Error("unable to record custom event", map[string]interface{}{ "event-type": "AppState", "reason": err.Error(), }) } md := LinkingMetadata{ EntityName: app.app.config.AppName, Hostname: app.app.config.hostname, EntityGUID: reply.Reply.EntityGUID, } return md } // StartTransaction begins a Transaction with the given name. func (app *Application) StartTransaction(name string, opts ...TraceOption) *Transaction { if app == nil { return nil } return app.app.StartTransaction(name, opts...) } // RecordCustomEvent adds a custom event. // // eventType must consist of alphanumeric characters, underscores, and // colons, and must contain fewer than 255 bytes. // // Each value in the params map must be a number, string, or boolean. // Keys must be less than 255 bytes. The params map may not contain // more than 64 attributes. For more information, and a set of // restricted keywords, see: // https://docs.newrelic.com/docs/insights/new-relic-insights/adding-querying-data/inserting-custom-events-new-relic-apm-agents // // An error is logged if eventType or params is invalid. func (app *Application) RecordCustomEvent(eventType string, params map[string]interface{}) { if app == nil || app.app == nil { return } err := app.app.RecordCustomEvent(eventType, params) if err != nil { app.app.Error("unable to record custom event", map[string]interface{}{ "event-type": eventType, "reason": err.Error(), }) } } // RecordLLMFeedbackEvent adds a LLM Feedback event. // An error is logged if eventType or params is invalid. func (app *Application) RecordLLMFeedbackEvent(trace_id string, rating any, category string, message string, metadata map[string]interface{}) { if app == nil || app.app == nil { return } CustomEventData := map[string]interface{}{ "trace_id": trace_id, "rating": rating, "category": category, "message": message, "ingest_source": "Go", } for k, v := range metadata { CustomEventData[k] = v } // if rating is an int or string, record the event err := app.app.RecordCustomEvent("LlmFeedbackMessage", CustomEventData) if err != nil { app.app.Error("unable to record custom event", map[string]interface{}{ "event-type": "LlmFeedbackMessage", "reason": err.Error(), }) } } // InvokeLLMTokenCountCallback invokes the function registered previously as the callback // function to compute token counts to report for LLM transactions, if any. If there is // no current callback funtion, this simply returns a zero count and a false boolean value. // Otherwise, it returns the value returned by the callback and a true value. // // Although there's no harm in calling this method to invoke your callback function, // there is no need (or particular benefit) of doing so. This is called as needed internally // by the AI Monitoring integrations. func (app *Application) InvokeLLMTokenCountCallback(model, content string) (int, bool) { if app == nil || app.app == nil || app.app.llmTokenCountCallback == nil { return 0, false } return app.app.llmTokenCountCallback(model, content), true } // HasLLMTokenCountCallback returns true if there is currently a registered callback function // or false otherwise. func (app *Application) HasLLMTokenCountCallback() bool { return app != nil && app.app != nil && app.app.llmTokenCountCallback != nil } // SetLLMTokenCountCallback registers a callback function which will be used by the AI Montoring // integration packages in cases where they are unable to determine the token counts directly. // You may call SetLLMTokenCountCallback multiple times. If you do, each call registers a new // callback function which replaces the previous one. Calling SetLLMTokenCountCallback(nil) removes // the callback function entirely. // // Your callback function will be passed two string parameters: model name and content. It must // return a single integer value which is the number of tokens to report. If it returns a value less // than or equal to zero, no token count report will be made (which includes the case where your // callback function was unable to determine the token count). func (app *Application) SetLLMTokenCountCallback(callbackFunction func(string, string) int) { if app != nil && app.app != nil { app.app.llmTokenCountCallback = callbackFunction } } // RecordCustomMetric records a custom metric. The metric name you // provide will be prefixed by "Custom/". Custom metrics are not // currently supported in serverless mode. // // See // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/collect-custom-metrics // for more information on custom events. func (app *Application) RecordCustomMetric(name string, value float64) { if app == nil || app.app == nil { return } err := app.app.RecordCustomMetric(name, value) if err != nil { app.app.Error("unable to record custom metric", map[string]interface{}{ "metric-name": name, "reason": err.Error(), }) } } // RecordLog records the data from a single log line. // This consumes a LogData object that should be configured // with data taken from a logging framework. // // Certian parts of this feature can be turned off based on your // config settings. Record log is capable of recording log events, // as well as log metrics depending on how your application is // configured. func (app *Application) RecordLog(logEvent LogData) { if app == nil || app.app == nil { return } err := app.app.RecordLog(&logEvent) if err != nil { app.app.Error("unable to record log", map[string]interface{}{ "reason": err.Error(), }) } } // WaitForConnection blocks until the application is connected, is // incapable of being connected, or the timeout has been reached. This // method is useful for short-lived processes since the application will // not gather data until it is connected. nil is returned if the // application is connected successfully. // // If Infinite Tracing is enabled, WaitForConnection will block until a // connection to the Trace Observer is made, a fatal error is reached, or the // timeout is hit. // // Note that in most cases, it is not necesary nor recommended to call // WaitForConnection() at all, particularly for any but the most trivial, short-lived // processes. It is better to simply start the application and allow the // instrumentation code to handle its connections on its own, which it will do // as needed in the background (and will continue attempting to connect // if it wasn't immediately successful, all while allowing your application // to proceed with its primary function). func (app *Application) WaitForConnection(timeout time.Duration) error { if app == nil || app.app == nil { return nil } return app.app.WaitForConnection(timeout) } // Shutdown flushes data to New Relic's servers and stops all // agent-related goroutines managing this application. After Shutdown // is called, the Application is disabled and will never collect data // again. This method blocks until all final data is sent to New Relic // or the timeout has elapsed. Increase the timeout and check debug // logs if you aren't seeing data. // // If Infinite Tracing is enabled, Shutdown will block until all queued span // events have been sent to the Trace Observer or the timeout has been reached. func (app *Application) Shutdown(timeout time.Duration) { if app == nil || app.app == nil { return } app.app.Shutdown(timeout) } // Config returns a copy of the application's configuration data in case // that information is needed (but since it is a copy, this function cannot // be used to alter the application's configuration). // // If the Config data could be copied from the application successfully, // a boolean true value is returned as the second return value. If it is // false, then the Config data returned is the standard default configuration. // This usually occurs if the Application is not yet fully initialized. func (app *Application) Config() (Config, bool) { if app == nil || app.app == nil { return defaultConfig(), false } return app.app.config.Config, true } func newApplication(app *app) *Application { return &Application{ app: app, Private: app, } } // NewApplication creates an Application and spawns goroutines to manage the // aggregation and harvesting of data. On success, a non-nil Application and a // nil error are returned. On failure, a nil Application and a non-nil error // are returned. All methods on an Application are nil safe. Therefore, a nil // Application pointer can be safely used. Applications do not share global // state, therefore it is safe to create multiple applications. // // The ConfigOption arguments allow for configuration of the Application. They // are applied in order from first to last, i.e. latter ConfigOptions may // overwrite the Config fields already set. func NewApplication(opts ...ConfigOption) (*Application, error) { c := defaultConfig() for _, fn := range opts { if fn != nil { fn(&c) if c.Error != nil { return nil, c.Error } } } cfg, err := newInternalConfig(c, os.Getenv, os.Environ()) if err != nil { return nil, err } return newApplication(newApp(cfg)), nil } go-agent-3.42.0/v3/newrelic/attributes.go000066400000000000000000000215631510742411500201650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic // This file contains the names of the automatically captured attributes. // Attributes are key value pairs attached to transaction events, error events, // traced errors, and spans. You may add your own attributes using the // Transaction.AddAttribute method (see transaction.go). // // These attribute names are exposed here to facilitate configuration. // // For more information, see: // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/agent-attributes // Attributes destined for Transaction Events, Errors, and Transaction Traces: const ( // AttributeResponseCode is the response status code for a web request. AttributeResponseCode = "http.statusCode" // AttributeResponseCodeDeprecated is the response status code for a web // request, the same value as AttributeResponseCode. To completely exclude // this value from a destination, both AttributeResponseCode and // AttributeResponseCodeDeprecated must be specified. // // Deprecated: This attribute is currently deprecated and will be removed // in a later release. AttributeResponseCodeDeprecated = "httpResponseCode" // AttributeRequestMethod is the request's method. AttributeRequestMethod = "request.method" // AttributeRequestAccept is the request's "Accept" header. AttributeRequestAccept = "request.headers.accept" // AttributeRequestContentType is the request's "Content-Type" header. AttributeRequestContentType = "request.headers.contentType" // AttributeRequestContentLength is the request's "Content-Length" header. AttributeRequestContentLength = "request.headers.contentLength" // AttributeRequestHost is the request's "Host" header. AttributeRequestHost = "request.headers.host" // AttributeRequestURI is the request's URL without query parameters, // fragment, user, or password. AttributeRequestURI = "request.uri" // AttributeResponseContentType is the response "Content-Type" header. AttributeResponseContentType = "response.headers.contentType" // AttributeResponseContentLength is the response "Content-Length" header. AttributeResponseContentLength = "response.headers.contentLength" // AttributeHostDisplayName contains the value of Config.HostDisplayName. AttributeHostDisplayName = "host.displayName" // AttributeCodeFunction contains the Code Level Metrics function name. AttributeCodeFunction = "code.function" // AttributeCodeNamespace contains the Code Level Metrics namespace name. AttributeCodeNamespace = "code.namespace" // AttributeCodeFilepath contains the Code Level Metrics source file path name. AttributeCodeFilepath = "code.filepath" // AttributeCodeLineno contains the Code Level Metrics source file line number name. AttributeCodeLineno = "code.lineno" // AttributeErrorGroupName contains the error group name set by the user defined callback function. AttributeErrorGroupName = "error.group.name" // AttributeUserID tracks the user a transaction and its child events are impacting AttributeUserID = "enduser.id" // AttributeLLM tracks LLM transactions AttributeLLM = "llm" ) // Attributes destined for Errors and Transaction Traces: const ( // AttributeRequestUserAgent is the request's "User-Agent" header. AttributeRequestUserAgent = "request.headers.userAgent" // AttributeRequestUserAgentDeprecated is the request's "User-Agent" // header, the same value as AttributeRequestUserAgent. To completely // exclude this value from a destination, both AttributeRequestUserAgent // and AttributeRequestUserAgentDeprecated must be specified. // // Deprecated: This attribute is currently deprecated and will be removed // in a later release. AttributeRequestUserAgentDeprecated = "request.headers.User-Agent" // AttributeRequestReferer is the request's "Referer" header. Query // string parameters are removed. AttributeRequestReferer = "request.headers.referer" ) // AWS Lambda specific attributes: const ( AttributeAWSRequestID = "aws.requestId" AttributeAWSLambdaARN = "aws.lambda.arn" AttributeAWSLambdaColdStart = "aws.lambda.coldStart" AttributeAWSLambdaEventSourceARN = "aws.lambda.eventSource.arn" ) // Attributes for consumed message transactions: // // When a message is consumed (for example from Kafka or RabbitMQ), supported // instrumentation packages -- i.e. those found in the v3/integrations // (https://godoc.org/github.com/newrelic/go-agent/v3/integrations) directory -- // will add these attributes automatically. AttributeMessageExchangeType, // AttributeMessageReplyTo, and AttributeMessageCorrelationID are disabled // by default. To see these attributes added to all destinations, you must add // include them in your config settings: // // cfg.Attributes.Include = append(cfg.Attributes.Include, // newrelic.AttributeMessageExchangeType, // newrelic.AttributeMessageReplyTo, // newrelic.AttributeMessageCorrelationID, // ) // // When not using a supported instrumentation package, you can add these // attributes manually using the Transaction.AddAttribute // (https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.AddAttribute) // API. In this case, these attributes will be included on all destintations // by default. // // txn := app.StartTransaction("Message/RabbitMQ/Exchange/Named/MyExchange") // txn.AddAttribute(newrelic.AttributeMessageRoutingKey, "myRoutingKey") // txn.AddAttribute(newrelic.AttributeMessageQueueName, "myQueueName") // txn.AddAttribute(newrelic.AttributeMessageExchangeType, "myExchangeType") // txn.AddAttribute(newrelic.AttributeMessageReplyTo, "myReplyTo") // txn.AddAttribute(newrelic.AttributeMessageCorrelationID, "myCorrelationID") // // ... consume a message ... // txn.End() // // It is recommended that at most one message is consumed per transaction. const ( // The account ID of a cloud service provider AttributeCloudAccountID = "cloud.account.id" // The region of a cloud service provider AttributeCloudRegion = "cloud.region" // The name of the messaging system AttributeMessageSystem = "messaging.system" // The name of the messagine broker destination AttributeMessageDestinationName = "message.destination.name" // The routing key of the consumed message. AttributeMessageRoutingKey = "message.routingKey" // The name of the queue the message was consumed from. AttributeMessageQueueName = "message.queueName" // The type of exchange used for the consumed message (direct, fanout, // topic, or headers). AttributeMessageExchangeType = "message.exchangeType" // The callback queue used in RPC configurations. AttributeMessageReplyTo = "message.replyTo" // The application-generated identifier used in RPC configurations. AttributeMessageCorrelationID = "message.correlationId" // The headers of the message without CAT keys/values AttributeMessageHeaders = "message.headers" // Host identifier of the message broker AttributeServerAddress = "server.address" // Port number of the message broker AttributeServerPort = "server.port" // Will take on either the values "producer" or "consumer" AttributeSpanKind = "span.kind" ) // Experimental OTEL Attributes for consumed message transactions const ( AttributeMessagingDestinationPublishName = "messaging.destination_publish.name" AttributeRabbitMQDestinationRoutingKey = "messaging.rabbitmq.destination.routing_key" ) // Attributes destined for Span Events. These attributes appear only on Span // Events and are not available to transaction events, error events, or traced // errors. // // To disable the capture of one of these span event attributes, "db.statement" // for example, modify your Config like this: // // cfg.SpanEvents.Attributes.Exclude = append(cfg.SpanEvents.Attributes.Exclude, // newrelic.SpanAttributeDBStatement) const ( SpanAttributeDBStatement = "db.statement" SpanAttributeDBInstance = "db.instance" SpanAttributeDBCollection = "db.collection" SpanAttributePeerAddress = "peer.address" SpanAttributePeerHostname = "peer.hostname" SpanAttributeHTTPURL = "http.url" SpanAttributeHTTPMethod = "http.method" SpanAttributeAWSOperation = "aws.operation" SpanAttributeAWSRegion = "aws.region" SpanAttributeErrorClass = "error.class" SpanAttributeErrorMessage = "error.message" SpanAttributeParentType = "parent.type" SpanAttributeParentApp = "parent.app" SpanAttributeParentAccount = "parent.account" SpanAttributeParentTransportDuration = "parent.transportDuration" SpanAttributeParentTransportType = "parent.transportType" // Deprecated: This attribute is a duplicate of AttributeResponseCode and // will be removed in a later release. SpanAttributeHTTPStatusCode = "http.statusCode" // Deprecated: This attribute is a duplicate of AttributeAWSRequestID and // will be removed in a later release. SpanAttributeAWSRequestID = "aws.requestId" ) go-agent-3.42.0/v3/newrelic/attributes_from_internal.go000066400000000000000000000457621510742411500231130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" "fmt" "math" "net/http" "net/url" "reflect" "sort" "strconv" "strings" "time" ) const ( // query parameters only appear in segments, not span events, but is // listed as span attributes to simplify code. It is not listed in the // public attributes.go file for this reason to prevent confusion. spanAttributeQueryParameters = "query_parameters" // The collector can only allow attributes to be a maximum of 256 bytes maxAttributeLengthBytes = 256 ) var ( usualDests = destAll &^ destBrowser tracesDests = destTxnTrace | destError // // To add an agent attribute, add it to the public constants in // attributes.go and add its default destinations here. // agentAttributeDefaultDests = map[string]destinationSet{ AttributeCloudAccountID: usualDests, AttributeMessageDestinationName: usualDests, AttributeCloudRegion: usualDests, AttributeMessageSystem: usualDests, AttributeHostDisplayName: usualDests, AttributeRequestMethod: usualDests, AttributeRequestAccept: usualDests, AttributeRequestContentType: usualDests, AttributeRequestContentLength: usualDests, AttributeRequestHost: usualDests, AttributeRequestUserAgent: tracesDests, AttributeRequestUserAgentDeprecated: tracesDests, AttributeRequestReferer: tracesDests, AttributeRequestURI: usualDests, AttributeResponseContentType: usualDests, AttributeResponseContentLength: usualDests, AttributeResponseCode: usualDests, AttributeResponseCodeDeprecated: usualDests, AttributeAWSRequestID: usualDests, AttributeAWSLambdaARN: usualDests, AttributeAWSLambdaColdStart: usualDests, AttributeAWSLambdaEventSourceARN: usualDests, AttributeMessageRoutingKey: usualDests, AttributeMessageQueueName: usualDests, AttributeMessageHeaders: usualDests, AttributeMessageExchangeType: destNone, AttributeMessageReplyTo: destNone, AttributeMessageCorrelationID: destNone, AttributeCodeFunction: usualDests, AttributeCodeNamespace: usualDests, AttributeCodeFilepath: usualDests, AttributeCodeLineno: usualDests, AttributeUserID: usualDests, AttributeLLM: usualDests, AttributeServerAddress: usualDests, AttributeServerPort: usualDests, AttributeSpanKind: usualDests, AttributeMessagingDestinationPublishName: usualDests, AttributeRabbitMQDestinationRoutingKey: usualDests, // Span specific attributes SpanAttributeDBStatement: usualDests, SpanAttributeDBInstance: usualDests, SpanAttributeDBCollection: usualDests, SpanAttributePeerAddress: usualDests, SpanAttributePeerHostname: usualDests, SpanAttributeHTTPURL: usualDests, SpanAttributeHTTPMethod: usualDests, spanAttributeQueryParameters: usualDests, SpanAttributeAWSOperation: usualDests, SpanAttributeAWSRegion: usualDests, SpanAttributeErrorClass: usualDests, SpanAttributeErrorMessage: usualDests, SpanAttributeParentType: usualDests, SpanAttributeParentApp: usualDests, SpanAttributeParentAccount: usualDests, SpanAttributeParentTransportDuration: usualDests, SpanAttributeParentTransportType: usualDests, } ) // https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md type destinationSet int const ( destTxnEvent destinationSet = 1 << iota destError destTxnTrace destBrowser destSpan destSegment ) const ( destNone destinationSet = 0 // destAll contains all destinations. destAll destinationSet = destTxnEvent | destTxnTrace | destError | destBrowser | destSpan | destSegment ) const ( attributeWildcardSuffix = '*' ) type attributeModifier struct { match string // This will not contain a trailing '*'. includeExclude } type byMatch []*attributeModifier func (m byMatch) Len() int { return len(m) } func (m byMatch) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m byMatch) Less(i, j int) bool { return m[i].match < m[j].match } // attributeConfig is created at connect and shared between all transactions. type attributeConfig struct { disabledDestinations destinationSet exactMatchModifiers map[string]*attributeModifier // Once attributeConfig is constructed, wildcardModifiers is sorted in // lexicographical order. Modifiers appearing later have precedence // over modifiers appearing earlier. wildcardModifiers []*attributeModifier agentDests map[string]destinationSet } type includeExclude struct { include destinationSet exclude destinationSet } func modifierApply(m *attributeModifier, d destinationSet) destinationSet { // Include before exclude, since exclude has priority. d |= m.include d &^= m.exclude return d } func applyAttributeConfig(c *attributeConfig, key string, d destinationSet) destinationSet { // Important: The wildcard modifiers must be applied before the exact // match modifiers, and the slice must be iterated in a forward // direction. for _, m := range c.wildcardModifiers { if strings.HasPrefix(key, m.match) { d = modifierApply(m, d) } } if m, ok := c.exactMatchModifiers[key]; ok { d = modifierApply(m, d) } d &^= c.disabledDestinations return d } func addModifier(c *attributeConfig, match string, d includeExclude) { if "" == match { return } exactMatch := true if attributeWildcardSuffix == match[len(match)-1] { exactMatch = false match = match[0 : len(match)-1] } mod := &attributeModifier{ match: match, includeExclude: d, } if exactMatch { if m, ok := c.exactMatchModifiers[mod.match]; ok { m.include |= mod.include m.exclude |= mod.exclude } else { c.exactMatchModifiers[mod.match] = mod } } else { for _, m := range c.wildcardModifiers { // Important: Duplicate entries for the same match // string would not work because exclude needs // precedence over include. if m.match == mod.match { m.include |= mod.include m.exclude |= mod.exclude return } } c.wildcardModifiers = append(c.wildcardModifiers, mod) } } func processDest(c *attributeConfig, includeEnabled bool, dc *AttributeDestinationConfig, d destinationSet) { if !dc.Enabled { c.disabledDestinations |= d } if includeEnabled { for _, match := range dc.Include { addModifier(c, match, includeExclude{include: d}) } } for _, match := range dc.Exclude { addModifier(c, match, includeExclude{exclude: d}) } } // createAttributeConfig creates a new attributeConfig. func createAttributeConfig(input config, includeEnabled bool) *attributeConfig { c := &attributeConfig{ exactMatchModifiers: make(map[string]*attributeModifier), wildcardModifiers: make([]*attributeModifier, 0, 64), } processDest(c, includeEnabled, &input.Attributes, destAll) processDest(c, includeEnabled, &input.ErrorCollector.Attributes, destError) processDest(c, includeEnabled, &input.TransactionEvents.Attributes, destTxnEvent) processDest(c, includeEnabled, &input.TransactionTracer.Attributes, destTxnTrace) processDest(c, includeEnabled, &input.BrowserMonitoring.Attributes, destBrowser) processDest(c, includeEnabled, &input.SpanEvents.Attributes, destSpan) processDest(c, includeEnabled, &input.TransactionTracer.Segments.Attributes, destSegment) sort.Sort(byMatch(c.wildcardModifiers)) c.agentDests = make(map[string]destinationSet) for name, dest := range agentAttributeDefaultDests { c.agentDests[name] = applyAttributeConfig(c, name, dest) } return c } type userAttribute struct { value interface{} dests destinationSet } type agentAttributeValue struct { stringVal string otherVal interface{} } type agentAttributes map[string]agentAttributeValue func (a *attributes) filterSpanAttributes(s map[string]jsonWriter, d destinationSet) map[string]jsonWriter { if nil != a { for key := range s { if a.config.agentDests[key]&d == 0 { delete(s, key) } } } return s } // GetAgentValue is used to access agent attributes. This function returns ("", // nil) if the attribute doesn't exist or it doesn't match the destinations // provided. func (a *attributes) GetAgentValue(id string, d destinationSet) (string, interface{}) { if nil == a || 0 == a.config.agentDests[id]&d { return "", nil } v, _ := a.Agent[id] return v.stringVal, v.otherVal } // Add is used to add agent attributes. Only one of stringVal and // otherVal should be populated. Since most agent attribute values are strings, // stringVal exists to avoid allocations. func (attr agentAttributes) Add(id string, stringVal string, otherVal interface{}) { if stringVal != "" || otherVal != nil { attr[id] = agentAttributeValue{ stringVal: truncateStringValueIfLong(stringVal), otherVal: otherVal, } } } // Remove is used to remove agent attributes. // It is not an error if the attribute wasn't present to begin with. func (attr agentAttributes) Remove(id string) { if _, ok := attr[id]; ok { delete(attr, id) } } // attributes are key value pairs attached to the various collected data types. type attributes struct { config *attributeConfig user map[string]userAttribute Agent agentAttributes } // newAttributes creates a new Attributes. func newAttributes(config *attributeConfig) *attributes { return &attributes{ config: config, Agent: make(agentAttributes), } } // errInvalidAttributeType is returned when the value is not valid. type errInvalidAttributeType struct { key string val interface{} } func (e errInvalidAttributeType) Error() string { return fmt.Sprintf("attribute '%s' value of type %T is invalid", e.key, e.val) } type invalidAttributeKeyErr struct{ key string } func (e invalidAttributeKeyErr) Error() string { return fmt.Sprintf("attribute key '%.32s...' exceeds length limit %d", e.key, attributeKeyLengthLimit) } type userAttributeLimitErr struct{ key string } func (e userAttributeLimitErr) Error() string { return fmt.Sprintf("attribute '%s' discarded: limit of %d reached", e.key, attributeUserLimit) } type invalidFloatAttrValue struct { key string val float64 } func (e invalidFloatAttrValue) Error() string { return fmt.Sprintf("attribute '%s' of type float contains an invalid value: %f", e.key, e.val) } func truncateStringValueIfLong(val string) string { if len(val) > attributeValueLengthLimit { return stringLengthByteLimit(val, attributeValueLengthLimit) } return val } func truncateStringMessageIfLong(message string) string { if len(message) > errorEventMessageLengthLimit { return stringLengthByteLimit(message, errorEventMessageLengthLimit) } return message } // validateUserAttribute validates a user attribute. func validateUserAttribute(key string, val interface{}) (interface{}, error) { if str, ok := val.(string); ok { val = interface{}(truncateStringValueIfLong(str)) } switch v := val.(type) { case string, bool, uint8, uint16, uint32, uint64, int8, int16, int32, int64, uint, int, uintptr: case float32: if err := validateFloat(float64(v), key); err != nil { return nil, err } case float64: if err := validateFloat(v, key); err != nil { return nil, err } default: return nil, errInvalidAttributeType{ key: key, val: val, } } // Attributes whose keys are excessively long are dropped rather than // truncated to avoid worrying about the application of configuration to // truncated values or performing the truncation after configuration. if len(key) > attributeKeyLengthLimit { return nil, invalidAttributeKeyErr{key: key} } return val, nil } // validateUserAttributeUnlimitedSize validates a user attribute without truncating string values. func validateUserAttributeUnlimitedSize(key string, val interface{}) (interface{}, error) { switch v := val.(type) { case string, bool, uint8, uint16, uint32, uint64, int8, int16, int32, int64, uint, int, uintptr: case float32: if err := validateFloat(float64(v), key); err != nil { return nil, err } case float64: if err := validateFloat(v, key); err != nil { return nil, err } default: return nil, errInvalidAttributeType{ key: key, val: val, } } // Attributes whose keys are excessively long are dropped rather than // truncated to avoid worrying about the application of configuration to // truncated values or performing the truncation after configuration. if len(key) > attributeKeyLengthLimit { return nil, invalidAttributeKeyErr{key: key} } return val, nil } func validateFloat(v float64, key string) error { if math.IsInf(v, 0) || math.IsNaN(v) { return invalidFloatAttrValue{ key: key, val: v, } } return nil } // addUserAttribute adds a user attribute. func addUserAttribute(a *attributes, key string, val interface{}, d destinationSet) error { val, err := validateUserAttribute(key, val) if nil != err { return err } dests := applyAttributeConfig(a.config, key, d) if destNone == dests { return nil } if nil == a.user { a.user = make(map[string]userAttribute) } if _, exists := a.user[key]; !exists && len(a.user) >= attributeUserLimit { return userAttributeLimitErr{key} } // Note: Duplicates are overridden: last attribute in wins. a.user[key] = userAttribute{ value: val, dests: dests, } return nil } func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { switch v := val.(type) { case string: if len(v) > maxAttributeLengthBytes { v = v[:maxAttributeLengthBytes] } w.stringField(key, v) case error: value := v.Error() if len(value) > maxAttributeLengthBytes { value = value[:maxAttributeLengthBytes] } w.stringField(key, value) case bool: if v { w.rawField(key, `true`) } else { w.rawField(key, `false`) } case uint8: w.intField(key, int64(v)) case uint16: w.intField(key, int64(v)) case uint32: w.intField(key, int64(v)) case uint64: w.intField(key, int64(v)) case uint: w.intField(key, int64(v)) case uintptr: w.intField(key, int64(v)) case int8: w.intField(key, int64(v)) case int16: w.intField(key, int64(v)) case int32: w.intField(key, int64(v)) case int64: w.intField(key, v) case int: w.intField(key, int64(v)) case float32: w.floatField(key, float64(v)) case float64: w.floatField(key, v) case time.Time: writeAttributeValueJSON(w, key, v.String()) case time.Duration: writeAttributeValueJSON(w, key, v.String()) case time.Weekday: writeAttributeValueJSON(w, key, v.String()) case *time.Location: writeAttributeValueJSON(w, key, v.String()) case time.Month: writeAttributeValueJSON(w, key, v.String()) default: // attempt to construct a JSON string kind := reflect.ValueOf(v).Kind() if kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice || kind == reflect.Array { bytes, _ := json.Marshal(v) if len(bytes) > maxAttributeLengthBytes { bytes = bytes[:maxAttributeLengthBytes] } w.stringField(key, string(bytes)) } else { w.stringField(key, fmt.Sprintf("%T", v)) } } } func agentAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet, additionalAttributes ...map[string]string) { if a == nil { buf.WriteString("{}") return } w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') for id, val := range a.Agent { if a.config.agentDests[id]&d != 0 { if val.stringVal != "" { w.stringField(id, val.stringVal) } else { writeAttributeValueJSON(&w, id, val.otherVal) } } } // Add additional agent attributes to json for _, additionalAttribute := range additionalAttributes { for id, val := range additionalAttribute { w.stringField(id, val) } } buf.WriteByte('}') } func userAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet, extraAttributes map[string]interface{}) { buf.WriteByte('{') if nil != a { w := jsonFieldsWriter{buf: buf} for key, val := range extraAttributes { outputDest := applyAttributeConfig(a.config, key, d) if outputDest&d != 0 { writeAttributeValueJSON(&w, key, val) } } for name, atr := range a.user { if atr.dests&d != 0 { if _, found := extraAttributes[name]; found { continue } writeAttributeValueJSON(&w, name, atr.value) } } } buf.WriteByte('}') } // userAttributesStringJSON is only used for testing. func userAttributesStringJSON(a *attributes, d destinationSet, extraAttributes map[string]interface{}) string { estimate := len(a.user) * 128 buf := bytes.NewBuffer(make([]byte, 0, estimate)) userAttributesJSON(a, buf, d, extraAttributes) return buf.String() } // RequestAgentAttributes gathers agent attributes out of the request. func requestAgentAttributes(a *attributes, method string, hdrs http.Header, u *url.URL, host string) { a.Agent.Add(AttributeRequestMethod, method, nil) if nil != u { a.Agent.Add(AttributeRequestURI, safeURL(u), nil) } if nil == hdrs { return } a.Agent.Add(AttributeRequestAccept, hdrs.Get("Accept"), nil) a.Agent.Add(AttributeRequestContentType, hdrs.Get("Content-Type"), nil) a.Agent.Add(AttributeRequestUserAgent, hdrs.Get("User-Agent"), nil) a.Agent.Add(AttributeRequestUserAgentDeprecated, hdrs.Get("User-Agent"), nil) a.Agent.Add(AttributeRequestReferer, safeURLFromString(hdrs.Get("Referer")), nil) a.Agent.Add(AttributeRequestHost, host, nil) if l := getContentLengthFromHeader(hdrs); l >= 0 { a.Agent.Add(AttributeRequestContentLength, "", l) } } // responseHeaderAttributes gather agent attributes from the response headers. func responseHeaderAttributes(a *attributes, h http.Header) { if nil == h { return } a.Agent.Add(AttributeResponseContentType, h.Get("Content-Type"), nil) if l := getContentLengthFromHeader(h); l >= 0 { a.Agent.Add(AttributeResponseContentLength, "", l) } } var ( // statusCodeLookup avoids a strconv.Itoa call. statusCodeLookup = map[int]string{ 100: "100", 101: "101", 200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206", 300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307", 400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406", 407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413", 414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 428: "428", 429: "429", 431: "431", 451: "451", 500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 511: "511", } ) // responseCodeAttribute sets the response code agent attribute. func responseCodeAttribute(a *attributes, code int) { rc := statusCodeLookup[code] if rc == "" { rc = strconv.Itoa(code) } a.Agent.Add(AttributeResponseCode, "", code) a.Agent.Add(AttributeResponseCodeDeprecated, rc, nil) } go-agent-3.42.0/v3/newrelic/attributes_test.go000066400000000000000000000330231510742411500212160ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" "net/http" "strconv" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal/crossagent" ) type AttributeTestcase struct { Testname string `json:"testname"` Config struct { AttributesEnabled bool `json:"attributes.enabled"` AttributesInclude []string `json:"attributes.include"` AttributesExclude []string `json:"attributes.exclude"` BrowserAttributesEnabled bool `json:"browser_monitoring.attributes.enabled"` BrowserAttributesInclude []string `json:"browser_monitoring.attributes.include"` BrowserAttributesExclude []string `json:"browser_monitoring.attributes.exclude"` ErrorAttributesEnabled bool `json:"error_collector.attributes.enabled"` ErrorAttributesInclude []string `json:"error_collector.attributes.include"` ErrorAttributesExclude []string `json:"error_collector.attributes.exclude"` EventsAttributesEnabled bool `json:"transaction_events.attributes.enabled"` EventsAttributesInclude []string `json:"transaction_events.attributes.include"` EventsAttributesExclude []string `json:"transaction_events.attributes.exclude"` TracerAttributesEnabled bool `json:"transaction_tracer.attributes.enabled"` TracerAttributesInclude []string `json:"transaction_tracer.attributes.include"` TracerAttributesExclude []string `json:"transaction_tracer.attributes.exclude"` } `json:"config"` Key string `json:"input_key"` InputDestinations []string `json:"input_default_destinations"` ExpectedDestinations []string `json:"expected_destinations"` } var ( destTranslate = map[string]destinationSet{ "attributes": destAll, "transaction_events": destTxnEvent, "transaction_tracer": destTxnTrace, "error_collector": destError, "browser_monitoring": destBrowser, } ) func destinationsFromArray(dests []string) destinationSet { d := destNone for _, s := range dests { if x, ok := destTranslate[s]; ok { d |= x } } return d } func destToString(d destinationSet) string { if 0 == d { return "none" } out := "" for _, ds := range []struct { Name string Dest destinationSet }{ {Name: "event", Dest: destTxnEvent}, {Name: "trace", Dest: destTxnTrace}, {Name: "error", Dest: destError}, {Name: "browser", Dest: destBrowser}, {Name: "span", Dest: destSpan}, {Name: "segment", Dest: destSegment}, } { if 0 != d&ds.Dest { if "" == out { out = ds.Name } else { out = out + "," + ds.Name } } } return out } func runAttributeTestcase(t *testing.T, js json.RawMessage) { var tc AttributeTestcase tc.Config.AttributesEnabled = true tc.Config.BrowserAttributesEnabled = false tc.Config.ErrorAttributesEnabled = true tc.Config.EventsAttributesEnabled = true tc.Config.TracerAttributesEnabled = true if err := json.Unmarshal(js, &tc); nil != err { t.Error(err) return } config := config{} config.Attributes.Enabled = tc.Config.AttributesEnabled config.Attributes.Include = tc.Config.AttributesInclude config.Attributes.Exclude = tc.Config.AttributesExclude config.ErrorCollector.Attributes.Enabled = tc.Config.ErrorAttributesEnabled config.ErrorCollector.Attributes.Include = tc.Config.ErrorAttributesInclude config.ErrorCollector.Attributes.Exclude = tc.Config.ErrorAttributesExclude config.TransactionEvents.Attributes.Enabled = tc.Config.EventsAttributesEnabled config.TransactionEvents.Attributes.Include = tc.Config.EventsAttributesInclude config.TransactionEvents.Attributes.Exclude = tc.Config.EventsAttributesExclude config.BrowserMonitoring.Attributes.Enabled = tc.Config.BrowserAttributesEnabled config.BrowserMonitoring.Attributes.Include = tc.Config.BrowserAttributesInclude config.BrowserMonitoring.Attributes.Exclude = tc.Config.BrowserAttributesExclude config.TransactionTracer.Attributes.Enabled = tc.Config.TracerAttributesEnabled config.TransactionTracer.Attributes.Include = tc.Config.TracerAttributesInclude config.TransactionTracer.Attributes.Exclude = tc.Config.TracerAttributesExclude cfg := createAttributeConfig(config, true) inputDests := destinationsFromArray(tc.InputDestinations) expectedDests := destinationsFromArray(tc.ExpectedDestinations) out := applyAttributeConfig(cfg, tc.Key, inputDests) if out != expectedDests { t.Errorf(`name="%s" input="%s" expected="%s" got="%s"`, tc.Testname, destToString(inputDests), destToString(expectedDests), destToString(out)) } } func TestCrossAgentAttributes(t *testing.T) { var tcs []json.RawMessage err := crossagent.ReadJSON("attribute_configuration.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { runAttributeTestcase(t, tc) } } func TestWriteAttributeValueJSON(t *testing.T) { buf := &bytes.Buffer{} w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') writeAttributeValueJSON(&w, "a", `escape\me!`) writeAttributeValueJSON(&w, "a", true) writeAttributeValueJSON(&w, "a", false) writeAttributeValueJSON(&w, "a", uint8(1)) writeAttributeValueJSON(&w, "a", uint16(2)) writeAttributeValueJSON(&w, "a", uint32(3)) writeAttributeValueJSON(&w, "a", uint64(4)) writeAttributeValueJSON(&w, "a", uint(5)) writeAttributeValueJSON(&w, "a", uintptr(6)) writeAttributeValueJSON(&w, "a", int8(-1)) writeAttributeValueJSON(&w, "a", int16(-2)) writeAttributeValueJSON(&w, "a", int32(-3)) writeAttributeValueJSON(&w, "a", int64(-4)) writeAttributeValueJSON(&w, "a", int(-5)) writeAttributeValueJSON(&w, "a", float32(1.5)) writeAttributeValueJSON(&w, "a", float64(4.56)) writeAttributeValueJSON(&w, "duration", time.Duration(7)) writeAttributeValueJSON(&w, "time", time.Time{}.AddDate(2000, 1, 1)) writeAttributeValueJSON(&w, "weekday", time.Wednesday) writeAttributeValueJSON(&w, "month", time.April) writeAttributeValueJSON(&w, "location", time.FixedZone("LOC", 0)) buf.WriteByte('}') expect := compactJSONString(`{ "a":"escape\\me!", "a":true, "a":false, "a":1, "a":2, "a":3, "a":4, "a":5, "a":6, "a":-1, "a":-2, "a":-3, "a":-4, "a":-5, "a":1.5, "a":4.56, "duration":"7ns", "time":"2001-02-02 00:00:00 +0000 UTC", "weekday":"Wednesday", "month":"April", "location":"LOC" }`) js := buf.String() if js != expect { t.Error(js, expect) } } func TestValidAttributeTypes(t *testing.T) { testcases := []struct { Input interface{} Valid bool }{ // Valid attribute types. {Input: "string value", Valid: true}, {Input: true, Valid: true}, {Input: uint8(0), Valid: true}, {Input: uint16(0), Valid: true}, {Input: uint32(0), Valid: true}, {Input: uint64(0), Valid: true}, {Input: int8(0), Valid: true}, {Input: int16(0), Valid: true}, {Input: int32(0), Valid: true}, {Input: int64(0), Valid: true}, {Input: float32(0), Valid: true}, {Input: float64(0), Valid: true}, {Input: uint(0), Valid: true}, {Input: int(0), Valid: true}, {Input: uintptr(0), Valid: true}, // Invalid attribute types. {Input: nil, Valid: false}, {Input: struct{}{}, Valid: false}, {Input: &struct{}{}, Valid: false}, } for _, tc := range testcases { val, err := validateUserAttribute("key", tc.Input) _, invalid := err.(errInvalidAttributeType) if tc.Valid == invalid { t.Error(tc.Input, tc.Valid, val, err) } } } func TestUserAttributeValLength(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) atLimit := strings.Repeat("a", attributeValueLengthLimit) tooLong := atLimit + "a" err := addUserAttribute(attrs, `escape\me`, tooLong, destAll) if err != nil { t.Error(err) } js := userAttributesStringJSON(attrs, destAll, nil) if `{"escape\\me":"`+atLimit+`"}` != js { t.Error(js) } } func TestUserAttributeKeyLength(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) lengthyKey := strings.Repeat("a", attributeKeyLengthLimit+1) err := addUserAttribute(attrs, lengthyKey, 123, destAll) if _, ok := err.(invalidAttributeKeyErr); !ok { t.Error(err) } js := userAttributesStringJSON(attrs, destAll, nil) if `{}` != js { t.Error(js) } } func TestNumUserAttributesLimit(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) for i := 0; i < attributeUserLimit; i++ { s := strconv.Itoa(i) err := addUserAttribute(attrs, s, s, destAll) if err != nil { t.Fatal(err) } } err := addUserAttribute(attrs, "cant_add_me", 123, destAll) if _, ok := err.(userAttributeLimitErr); !ok { t.Fatal(err) } js := userAttributesStringJSON(attrs, destAll, nil) var out map[string]string err = json.Unmarshal([]byte(js), &out) if nil != err { t.Fatal(err) } if len(out) != attributeUserLimit { t.Error(len(out)) } if strings.Contains(js, "cant_add_me") { t.Fatal(js) } // Now test that replacement works when the limit is reached. err = addUserAttribute(attrs, "0", "BEEN_REPLACED", destAll) if nil != err { t.Fatal(err) } js = userAttributesStringJSON(attrs, destAll, nil) if !strings.Contains(js, "BEEN_REPLACED") { t.Fatal(js) } } func TestExtraAttributesIncluded(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) err := addUserAttribute(attrs, "a", 1, destAll) if nil != err { t.Error(err) } js := userAttributesStringJSON(attrs, destAll, map[string]interface{}{"b": 2}) if `{"b":2,"a":1}` != js { t.Error(js) } } func TestExtraAttributesPrecedence(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) err := addUserAttribute(attrs, "a", 1, destAll) if nil != err { t.Error(err) } js := userAttributesStringJSON(attrs, destAll, map[string]interface{}{"a": 2}) if `{"a":2}` != js { t.Error(js) } } func TestIncludeDisabled(t *testing.T) { input := config{Config: defaultConfig()} input.Attributes.Include = append(input.Attributes.Include, "include_me") cfg := createAttributeConfig(input, false) attrs := newAttributes(cfg) err := addUserAttribute(attrs, "include_me", 1, destNone) if nil != err { t.Error(err) } js := userAttributesStringJSON(attrs, destAll, nil) if `{}` != js { t.Error(js) } } func agentAttributesMap(attrs *attributes, d destinationSet) map[string]interface{} { buf := &bytes.Buffer{} agentAttributesJSON(attrs, buf, d) var m map[string]interface{} err := json.Unmarshal(buf.Bytes(), &m) if err != nil { panic(err) } return m } func TestRequestAgentAttributesEmptyInput(t *testing.T) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) requestAgentAttributes(attrs, "", nil, nil, "") got := agentAttributesMap(attrs, destAll) expectAttributes(t, got, map[string]interface{}{}) } func TestRequestAgentAttributesPresent(t *testing.T) { req, err := http.NewRequest("GET", "http://www.newrelic.com?remove=me", nil) if nil != err { t.Fatal(err) } req.Header.Set("Accept", "the-accept") req.Header.Set("Content-Type", "the-content-type") req.Header.Set("User-Agent", "the-agent") req.Header.Set("Referer", "http://www.example.com") req.Header.Set("Content-Length", "123") req.Host = "the-host" cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attrs := newAttributes(cfg) requestAgentAttributes(attrs, req.Method, req.Header, req.URL, req.Host) got := agentAttributesMap(attrs, destAll) expectAttributes(t, got, map[string]interface{}{ "request.headers.contentType": "the-content-type", "request.headers.host": "the-host", "request.headers.User-Agent": "the-agent", "request.headers.userAgent": "the-agent", "request.headers.referer": "http://www.example.com", "request.headers.contentLength": 123, "request.method": "GET", "request.uri": "http://www.newrelic.com", "request.headers.accept": "the-accept", }) } func BenchmarkAgentAttributes(b *testing.B) { cfg := createAttributeConfig(config{Config: defaultConfig()}, true) req, err := http.NewRequest("GET", "http://www.newrelic.com", nil) if nil != err { b.Fatal(err) } req.Header.Set("Accept", "zap") req.Header.Set("Content-Type", "zap") req.Header.Set("User-Agent", "zap") req.Header.Set("Referer", "http://www.newrelic.com") req.Header.Set("Content-Length", "123") req.Host = "zap" b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { attrs := newAttributes(cfg) requestAgentAttributes(attrs, req.Method, req.Header, req.URL, req.Host) buf := bytes.Buffer{} agentAttributesJSON(attrs, &buf, destTxnTrace) } } func TestGetAgentValue(t *testing.T) { // Test nil safe var attrs *attributes outstr, outother := attrs.GetAgentValue(AttributeRequestURI, destTxnTrace) if outstr != "" || outother != nil { t.Error(outstr, outother) } c := config{Config: defaultConfig()} c.TransactionTracer.Attributes.Exclude = []string{"request.uri"} cfg := createAttributeConfig(c, true) attrs = newAttributes(cfg) attrs.Agent.Add(AttributeResponseContentLength, "", 123) attrs.Agent.Add(AttributeRequestMethod, "GET", nil) attrs.Agent.Add(AttributeRequestURI, "/url", nil) // disabled by configuration outstr, outother = attrs.GetAgentValue(AttributeResponseContentLength, destTxnTrace) if outstr != "" || outother != 123 { t.Error(outstr, outother) } outstr, outother = attrs.GetAgentValue(AttributeRequestMethod, destTxnTrace) if outstr != "GET" || outother != nil { t.Error(outstr, outother) } outstr, outother = attrs.GetAgentValue(AttributeRequestURI, destTxnTrace) if outstr != "" || outother != nil { t.Error(outstr, outother) } } go-agent-3.42.0/v3/newrelic/browser_header.go000066400000000000000000000060241510742411500207650ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" ) var ( browserStartTag = []byte(``) browserInfoPrefix = []byte(`window.NREUM||(NREUM={});NREUM.info=`) ) // browserInfo contains the fields that are marshalled into the Browser agent's // info hash. // // https://newrelic.atlassian.net/wiki/spaces/eng/pages/50299103/BAM+Agent+Auto-Instrumentation type browserInfo struct { Beacon string `json:"beacon"` LicenseKey string `json:"licenseKey"` ApplicationID string `json:"applicationID"` TransactionName string `json:"transactionName"` QueueTimeMillis int64 `json:"queueTime"` ApplicationTimeMillis int64 `json:"applicationTime"` ObfuscatedAttributes string `json:"atts"` ErrorBeacon string `json:"errorBeacon"` Agent string `json:"agent"` } // BrowserTimingHeader encapsulates the JavaScript required to enable New // Relic's Browser product. type BrowserTimingHeader struct { agentLoader string info browserInfo } func appendSlices(slices ...[]byte) []byte { length := 0 for _, s := range slices { length += len(s) } combined := make([]byte, 0, length) for _, s := range slices { combined = append(combined, s...) } return combined } // WithTags returns the browser timing JavaScript which includes the enclosing // tags. This method returns nil if the receiver is // nil, the feature is disabled, the application is not yet connected, or an // error occurs. The byte slice returned is in UTF-8 format. func (h *BrowserTimingHeader) WithTags() []byte { withoutTags := h.WithoutTags() if nil == withoutTags { return nil } return appendSlices(browserStartTag, withoutTags, browserEndTag) } // WithoutTags returns the browser timing JavaScript without any enclosing tags, // which may then be embedded within any JavaScript code. This method returns // nil if the receiver is nil, the feature is disabled, the application is not // yet connected, or an error occurs. The byte slice returned is in UTF-8 // format. func (h *BrowserTimingHeader) WithoutTags() []byte { if nil == h { return nil } // We could memoise this, but it seems unnecessary, since most users are // going to call this zero or one times. info, err := json.Marshal(h.info) if err != nil { // There's no way to log from here, but this also should be unreachable in // practice. return nil } return appendSlices([]byte(h.agentLoader), browserInfoPrefix, info) } // browserAttributes returns a string with the attributes that are attached to // the browser destination encoded in the JSON format expected by the Browser // agent. func browserAttributes(a *attributes) []byte { buf := &bytes.Buffer{} buf.WriteString(`{"u":`) userAttributesJSON(a, buf, destBrowser, nil) buf.WriteString(`,"a":`) agentAttributesJSON(a, buf, destBrowser) buf.WriteByte('}') return buf.Bytes() } go-agent-3.42.0/v3/newrelic/browser_header_test.go000066400000000000000000000053241510742411500220260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "fmt" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestNilBrowserTimingHeader(t *testing.T) { var h *BrowserTimingHeader // The methods on a nil BrowserTimingHeader pointer should not panic. if out := h.WithTags(); out != nil { t.Errorf("unexpected WithTags output for a disabled header: expected a blank string; got %s", out) } if out := h.WithoutTags(); out != nil { t.Errorf("unexpected WithoutTags output for a disabled header: expected a blank string; got %s", out) } } func TestEnabled(t *testing.T) { // We're not trying to test Go's JSON marshalling here; we just want to // ensure that we get the right fields out the other side. expectInfo := internal.CompactJSONString(` { "beacon": "brecon", "licenseKey": "12345", "applicationID": "app", "transactionName": "txn", "queueTime": 1, "applicationTime": 2, "atts": "attrs", "errorBeacon": "blah", "agent": "bond" } `) h := &BrowserTimingHeader{ agentLoader: "loader();", info: browserInfo{ Beacon: "brecon", LicenseKey: "12345", ApplicationID: "app", TransactionName: "txn", QueueTimeMillis: 1, ApplicationTimeMillis: 2, ObfuscatedAttributes: "attrs", ErrorBeacon: "blah", Agent: "bond", }, } expected := fmt.Sprintf("%s%s%s%s%s", browserStartTag, h.agentLoader, browserInfoPrefix, expectInfo, browserEndTag) if actual := h.WithTags(); string(actual) != expected { t.Errorf("unexpected WithTags output: expected %s; got %s", expected, string(actual)) } expected = fmt.Sprintf("%s%s%s", h.agentLoader, browserInfoPrefix, expectInfo) if actual := h.WithoutTags(); string(actual) != expected { t.Errorf("unexpected WithoutTags output: expected %s; got %s", expected, string(actual)) } } func TestBrowserAttributesNil(t *testing.T) { expected := `{"u":{},"a":{}}` actual := string(browserAttributes(nil)) if expected != actual { t.Errorf("unexpected browser attributes: expected %s; got %s", expected, actual) } } func TestBrowserAttributes(t *testing.T) { config := config{Config: defaultConfig()} config.BrowserMonitoring.Attributes.Enabled = true a := newAttributes(createAttributeConfig(config, true)) addUserAttribute(a, "user", "thing", destBrowser) addUserAttribute(a, "not", "shown", destError) a.Agent.Add(AttributeHostDisplayName, "host", nil) expected := `{"u":{"user":"thing"},"a":{}}` actual := string(browserAttributes(a)) if expected != actual { t.Errorf("unexpected browser attributes: expected %s; got %s", expected, actual) } } go-agent-3.42.0/v3/newrelic/cat_test.go000066400000000000000000000123411510742411500175770ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/crossagent" ) type eventAttributes map[string]interface{} func (e eventAttributes) has(key string) bool { _, ok := e[key] return ok } func (e eventAttributes) isString(key string, expected string) error { actual, ok := e[key].(string) if !ok { return fmt.Errorf("key %s is not a string; got type %t with value %v", key, e[key], e[key]) } if actual != expected { return fmt.Errorf("key %s has unexpected value: expected=%s; got=%s", key, expected, actual) } return nil } type harvestedTxnEvent struct { intrinsics eventAttributes userAttributes eventAttributes agentAttributes eventAttributes } func (h *harvestedTxnEvent) UnmarshalJSON(data []byte) error { var arr []eventAttributes if err := json.Unmarshal(data, &arr); err != nil { return err } if len(arr) != 3 { return fmt.Errorf("unexpected number of transaction event items: %d", len(arr)) } h.intrinsics = arr[0] h.userAttributes = arr[1] h.agentAttributes = arr[2] return nil } func harvestTxnDataEvent(t *txnData) (*harvestedTxnEvent, error) { // Since transaction event JSON is built using string manipulation, we have // to do an awkward marshal/unmarshal shuffle to be able to verify the // intrinsics. js, err := json.Marshal(&t.txnEvent) if err != nil { return nil, err } event := &harvestedTxnEvent{} if err := json.Unmarshal(js, event); err != nil { return nil, err } return event, nil } // This function implements as close as we can get to the round trip tests in // the cross agent tests. func TestCatMap(t *testing.T) { var testcases []struct { Name string `json:"name"` AppName string `json:"appName"` TransactionName string `json:"transactionName"` TransactionGUID string `json:"transactionGuid"` InboundPayload []interface{} `json:"inboundPayload"` ExpectedIntrinsicFields map[string]string `json:"expectedIntrinsicFields"` NonExpectedIntrinsicFields []string `json:"nonExpectedIntrinsicFields"` OutboundRequests []struct { OutboundTxnName string `json:"outboundTxnName"` ExpectedOutboundPayload json.RawMessage `json:"expectedOutboundPayload"` } `json:"outboundRequests"` } err := crossagent.ReadJSON("cat/cat_map.json", &testcases) if err != nil { t.Fatal(err) } for _, tc := range testcases { // Fake enough transaction data to run the test. tr := &txnData{ Name: tc.TransactionName, } tr.CrossProcess.Init(true, false, &internal.ConnectReply{ CrossProcessID: "1#1", EncodingKey: "foo", TrustedAccounts: map[int]struct{}{1: {}}, }) // Marshal the inbound payload into JSON for easier testing. txnData, err := json.Marshal(tc.InboundPayload) if err != nil { t.Errorf("%s: error marshalling inbound payload: %v", tc.Name, err) } // Set up the GUID. if tc.TransactionGUID != "" { tr.CrossProcess.GUID = tc.TransactionGUID } // Swallow errors, since some of these tests are testing the behaviour when // erroneous headers are provided. tr.CrossProcess.handleInboundRequestTxnData(txnData) // Simulate outbound requests. for _, req := range tc.OutboundRequests { metadata, err := tr.CrossProcess.CreateCrossProcessMetadata(req.OutboundTxnName, tc.AppName) if err != nil { t.Errorf("%s: error creating outbound request headers: %v", tc.Name, err) } // Grab and deobfuscate the txndata that would have been sent to the // external service. txnData, err := deobfuscate(metadata.TxnData, tr.CrossProcess.EncodingKey) if err != nil { t.Errorf("%s: error deobfuscating outbound request header: %v", tc.Name, err) } // Check the JSON against the expected value. compacted := compactJSONString(string(txnData)) expected := compactJSONString(string(req.ExpectedOutboundPayload)) if compacted != expected { t.Errorf("%s: outbound metadata does not match expected value: expected=%s; got=%s", tc.Name, expected, compacted) } } // Finalise the transaction, ignoring errors. tr.CrossProcess.Finalise(tc.TransactionName, tc.AppName) // Harvest the event. event, err := harvestTxnDataEvent(tr) if err != nil { t.Errorf("%s: error harvesting event data: %v", tc.Name, err) } // Now we have the event, let's look for the expected intrinsics. for key, value := range tc.ExpectedIntrinsicFields { // First, check if the key exists at all. if !event.intrinsics.has(key) { t.Fatalf("%s: missing intrinsic %s", tc.Name, key) } // Everything we're looking for is a string, so we can be a little lazy // here. if err := event.intrinsics.isString(key, value); err != nil { t.Errorf("%s: %v", tc.Name, err) } } // Finally, we verify that the unexpected intrinsics didn't miraculously // appear. for _, key := range tc.NonExpectedIntrinsicFields { if event.intrinsics.has(key) { t.Errorf("%s: expected intrinsic %s to be missing; instead, got value %v", tc.Name, key, event.intrinsics[key]) } } } } go-agent-3.42.0/v3/newrelic/code_level_metrics.go000066400000000000000000000532711510742411500216270ustar00rootroot00000000000000// Copyright 2022 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "reflect" "runtime" "strings" "sync" ) // defaultAgentProjectRoot is the default filename pattern which is at // the root of the agent's import path. This is used to identify functions // on the call stack which are assumed to belong to the agent rather than // the instrumented application's code. const defaultAgentProjectRoot = "github.com/newrelic/go-agent/" // CodeLocation marks the location of a line of source code for later reference. type CodeLocation struct { // LineNo is the line number within the source file. LineNo int // Function is the function name (note that this may be auto-generated by Go // for function literals and the like). This is the fully-qualified name, which // includes the package name and other information to unambiguously identify // the function. Function string // FilePath is the absolute pathname on disk of the source file referred to. FilePath string } // CachedCodeLocation provides storage for the code location computed such that // the discovery of the code location is only done once; thereafter the cached // value is available for use. // // This type includes methods with the same names as some of the basic code location // reporting functions and TraceOptions. However, when called as methods of a CachedCodeLocation value // instead of a stand-alone function, the operation will make use of the cache to // prevent computing the same source location more than once. // // A usable CachedCodeLocation value must be obtained via a call to // NewCachedCodeLocation. type CachedCodeLocation struct { location *CodeLocation once *sync.Once err error } // Err returns the error condition encountered when trying to determine // the code location being cached, if any. func (c *CachedCodeLocation) Err() error { if c == nil { return errors.New("nil CachedCodeLocation") } return c.err } // IsValid returns true if the cache value was correctly initialized // (as by, for example, NewCachedCodeLocation), and therefore can be // used to cache code location values. Otherwise it cannot be used. func (c *CachedCodeLocation) IsValid() bool { return c != nil && c.once != nil } // NewCachedCodeLocation returns a pointer to a newly-created // CachedCodeLocation value, suitable for use with the methods // defined for that type. func NewCachedCodeLocation() *CachedCodeLocation { return &CachedCodeLocation{ once: new(sync.Once), } } type traceOptSet struct { LocationOverride *CodeLocation SuppressCLM bool DemandCLM bool IgnoredPrefixes []string PathPrefixes []string LocationCallback func() *CodeLocation } // TraceOption values provide optional parameters to transactions. // // (Currently it's only implemented for transactions, but the name TraceOption is // intentionally generic in case we apply these to other kinds of traces in the future.) type TraceOption func(*traceOptSet) // WithCodeLocation adds an explicit CodeLocation value // to report for the Code Level Metrics attached to a trace. // This is probably a value previously obtained by calling // ThisCodeLocation(). // // Deprecated: This function requires the caller to do the work // up-front to calculate the code location, which may be a waste // of effort if code level metrics happens to be disabled. Instead, // use the WithCodeLocationCallback function. func WithCodeLocation(loc *CodeLocation) TraceOption { return func(o *traceOptSet) { o.LocationOverride = loc } } // WithCodeLocationCallback adds a callback function which the agent // will call if it needs to report the code location with an explicit // value provided by the caller. This will only be called if code // level metrics is enabled, saving unnecessary work if those metrics // are not enabled. // // If the callback function value passed here is nil, then no callback // function will be used (same as if this function were never called). // If the callback function itself returns nil instead of a pointer to // a CodeLocation, then it is assumed the callback function was not able // to determine the code location, and the CLM reporting code's normal // method for determining the code location is used instead. func WithCodeLocationCallback(locf func() *CodeLocation) TraceOption { return func(o *traceOptSet) { o.LocationCallback = locf } } // WithIgnoredPrefix indicates that the code location reported // for Code Level Metrics should be the first function in the // call stack that does not begin with the given string (or any of the given strings if more than one are given). This // string is matched against the entire fully-qualified function // name, which includes the name of the package the function // comes from. By default, the Go Agent tries to take the first // function on the call stack that doesn't seem to be internal to // the agent itself, but you can control this behavior using // this option. // // If all functions in the call stack begin with this prefix, // the outermost one will be used anyway, since we didn't find // anything better on the way to the bottom of the stack. // // If no prefix strings are passed here, the configured defaults will be used. // // Deprecated: New code should use WithIgnoredPrefixes instead. func WithIgnoredPrefix(prefix ...string) TraceOption { return func(o *traceOptSet) { o.IgnoredPrefixes = prefix } } // WithIgnoredPrefixes indicates that the code location reported // for Code Level Metrics should be the first function in the // call stack that does not begin with the given string (or any of the given strings if more than one are given). This // string is matched against the entire fully-qualified function // name, which includes the name of the package the function // comes from. By default, the Go Agent tries to take the first // function on the call stack that doesn't seem to be internal to // the agent itself, but you can control this behavior using // this option. // // If all functions in the call stack begin with this prefix, // the outermost one will be used anyway, since we didn't find // anything better on the way to the bottom of the stack. // // If no prefix strings are passed here, the configured defaults will be used. func WithIgnoredPrefixes(prefix ...string) TraceOption { return func(o *traceOptSet) { o.IgnoredPrefixes = prefix } } // WithPathPrefix overrides the list of source code path prefixes // used to trim source file pathnames, providing a new set of one // or more path prefixes to use for this trace only. // If no strings are given, the configured defaults will be used. // // Deprecated: New code should use WithPathPrefixes instead. func WithPathPrefix(prefix ...string) TraceOption { return func(o *traceOptSet) { o.PathPrefixes = prefix } } // WithPathPrefixes overrides the list of source code path prefixes // used to trim source file pathnames, providing a new set of one // or more path prefixes to use for this trace only. // If no strings are given, the configured defaults will be used. func WithPathPrefixes(prefix ...string) TraceOption { return func(o *traceOptSet) { o.PathPrefixes = prefix } } // WithoutCodeLevelMetrics suppresses the collection and reporting // of Code Level Metrics for this trace. This helps avoid the overhead // of collecting that information if it's not needed for certain traces. func WithoutCodeLevelMetrics() TraceOption { return func(o *traceOptSet) { o.SuppressCLM = true } } // WithCodeLevelMetrics includes this trace in code level metrics even if // it would otherwise not be (for example, if it would be out of the configured // scope setting). This will never cause code level metrics to be reported if // CLM were explicitly disabled (e.g. by CLM being globally off or if WithoutCodeLevelMetrics // is present in the options for this trace). func WithCodeLevelMetrics() TraceOption { return func(o *traceOptSet) { o.DemandCLM = true } } // WithThisCodeLocation is equivalent to calling WithCodeLocation, referring // to the point in the code where the WithThisCodeLocation call is being made. // This can be helpful, for example, when the actual code invocation which starts // a transaction or other kind of trace is originating from a framework or other // centralized location, but you want to report this point in your application // for the Code Level Metrics associated with this trace. func WithThisCodeLocation() TraceOption { return WithCodeLocation(ThisCodeLocation()) } // WithThisCodeLocation is equivalent to the standalone WithThisCodeLocation // TraceOption, but uses the cached value in its receiver to ensure that the // overhead of computing the code location is only performed the first time // it is invoked for each instance of the receiver variable. func (c *CachedCodeLocation) WithThisCodeLocation() TraceOption { return WithCodeLocation(c.ThisCodeLocation()) } // FunctionLocation is like ThisCodeLocation, but takes as its parameter // a function value. It will report the code-level metrics information for // that function if that is possible to do. It returns an error if it // was not possible to get a code location from the parameter passed to it. // // If multiple functions are passed, each will be attempted until one is // found for which we can successfully find a code location. func FunctionLocation(functions ...interface{}) (*CodeLocation, error) { for _, function := range functions { if function == nil { continue } v := reflect.ValueOf(function) if !v.IsValid() || v.Kind() != reflect.Func { continue } if fInfo := runtime.FuncForPC(v.Pointer()); fInfo != nil { var loc CodeLocation loc.FilePath, loc.LineNo = fInfo.FileLine(fInfo.Entry()) loc.Function = fInfo.Name() return &loc, nil } } return nil, errors.New("could not find code location for function") } // FunctionLocation works identically to the stand-alone FunctionLocation function, // in that it determines the souce code location of the named function, returning // a pointer to a CodeLocation value which represents that location, or an error value // if it was unable to find a valid code location for the provided value. However, // unlike the stand-alone function, this stores the result in the CachedCodeLocation receiver; // thus, subsequent invocations of FunctionLocation for the same receiver will result in // immediately repeating the value (or error, if applicable) obtained from the first // invocation. // // This is thread-safe and is intended to allow the same code to run in multiple // concurrent goroutines without needlessly recalculating the location of the // function value. func (c *CachedCodeLocation) FunctionLocation(functions ...interface{}) (*CodeLocation, error) { if c == nil || !c.IsValid() { // The cache is bogus so don't use it return FunctionLocation(functions...) } c.once.Do(func() { c.location, c.err = FunctionLocation(functions...) }) return c.location, c.err } // WithFunctionLocation is like WithThisCodeLocation, but uses the // function value(s) passed as the location to report. Unlike FunctionLocation, // this does not report errors explicitly. If it is unable to use the // value passed to find a code location, it will do nothing. func WithFunctionLocation(functions ...interface{}) TraceOption { return func(o *traceOptSet) { loc, err := FunctionLocation(functions...) if err == nil { o.LocationOverride = loc } } } // WithFunctionLocation works like the standalone function WithFunctionLocation, // but it stores a copy of the function's location in its receiver the first time // it is used. Subsequently that cached value will be used instead of computing // the source code location every time. // // This is thread-safe and is intended to allow the same code to run in multiple // concurrent goroutines without needlessly recalculating the location of the // function value. func (c *CachedCodeLocation) WithFunctionLocation(functions ...interface{}) TraceOption { return func(o *traceOptSet) { loc, err := c.FunctionLocation(functions...) if err == nil { o.LocationOverride = loc } } } // WithDefaultFunctionLocation is like WithFunctionLocation but will only // evaluate the location of the function if nothing that came before it // set a code location first. This is useful, for example, if you want to // provide a default code location value to be used but not pay the overhead // of resolving that location until it's clear that you will need to. This // should appear at the end of a TraceOption list (or at least after any // other options that want to specify the code location). func WithDefaultFunctionLocation(functions ...interface{}) TraceOption { return func(o *traceOptSet) { if o.LocationOverride == nil { WithFunctionLocation(functions...)(o) } } } // WithDefaultFunctionLocation works like the standalone WithDefaultFunctionLocation function, // except that it takes a CachedCodeLocation receiver which will // be used to cache the source code location of the function value. // // Thus, this will arrange for the given function to be reported in Code Level Metrics // only if no other option that came before it gave an explicit location to use instead, // but will also cache that answer in the provided CachedCodeLocation receiver variable, so that // if called again with the same CachedCodeLocation variable, it will avoid the overhead // of finding the function's location again, using instead the cached answer. // // This is thread-safe and is intended to allow the same code to run in multiple // concurrent goroutines without needlessly recalculating the location of the // function value. // // If an error is encountered when trying to evaluate the source code location of // the provided function value, WithCachedDefaultFunctionLocation will not set anything // for the reported code location, and the error will be available as a non-nil value // in the Err member of the CachedCodeLocation variable. // In this case, no additional attempts are guaranteed to be made on subsequent executions // to determine the code location. func (c *CachedCodeLocation) WithDefaultFunctionLocation(functions ...interface{}) TraceOption { return func(o *traceOptSet) { if o.LocationOverride == nil { loc, err := c.FunctionLocation(functions...) if err == nil { WithCodeLocation(loc)(o) } } } } // withPreparedOptions copies the option settings from a structure // which was already set up (probably by executing a set of TraceOption // functions already). func withPreparedOptions(newOptions *traceOptSet) TraceOption { return func(o *traceOptSet) { if newOptions != nil { if newOptions.LocationOverride != nil { o.LocationOverride = newOptions.LocationOverride } if newOptions.LocationCallback != nil { o.LocationCallback = newOptions.LocationCallback } o.SuppressCLM = newOptions.SuppressCLM o.DemandCLM = newOptions.DemandCLM if newOptions.IgnoredPrefixes != nil { o.IgnoredPrefixes = newOptions.IgnoredPrefixes } if newOptions.PathPrefixes != nil { o.PathPrefixes = newOptions.PathPrefixes } } } } // ThisCodeLocation returns a CodeLocation value referring to // the place in your code that it was invoked. // // With no arguments (or if passed a 0 value), it returns the location // of its own caller. However, you may adjust this by passing the number // of function calls to skip. For example, ThisCodeLocation(1) will return // the CodeLocation of the place the current function was called from // (i.e., the caller of the caller of ThisCodeLocation). func ThisCodeLocation(skipLevels ...int) *CodeLocation { skip := 0 if len(skipLevels) > 0 { skip += skipLevels[0] } return thisCodeLocationCommon(skip, false) } func thisCodeLocationCommon(skip int, skipInternal bool) *CodeLocation { var loc CodeLocation pcs := make([]uintptr, 20) depth := runtime.Callers(1, pcs) if depth > 0 { var frame runtime.Frame var clmFile string stillMore := true skipCLM := true frames := runtime.CallersFrames(pcs[:depth]) for stillMore { frame, stillMore = frames.Next() // // We will begin here in our own CLM module code. We don't need to know // the IgnoredPrefix value since we can see the actual filename here now. // The first function in the call stack will be from here in the CLM code // so remember it for later. // if clmFile == "" { clmFile = frame.File } // // We need to skip over all the functions internal to this CLM module // to get to the function being reported. // if skipCLM && frame.File == clmFile { continue } skipCLM = false // // Now that we're past our CLM code, we might need to skip past an intermediary // set of calls that we entered (e.g., sync.(*Once)). // if skipInternal { if frame.File != clmFile { continue } // // past that, and back in our CLM code again, which we now also // need to skip as well. // skipInternal = false skipCLM = true continue } // // This should now be into the user's code. If they asked us to skip // over a number of calls, do that. // if skip > 0 { skip-- continue } // // Finally, we have arrived at the target function. // stillMore = false } loc.LineNo = frame.Line loc.Function = frame.Function loc.FilePath = frame.File } return &loc } // ThisCodeLocation works identically to the stand-alone ThisCodeLocation function, // in that it determines the souce code location from whence it was called, returning // a pointer to a CodeLocation value which represents that location. However, // unlike the stand-alone function, this stores the result in the CachedCodeLocation receiver; // thus, subsequent invocations of ThisCodeLocation for the same receiver will result in // immediately repeating the value obtained from the first // invocation. // // This is thread-safe and is intended to allow the same code to run in multiple // concurrent goroutines without needlessly recalculating the location of the // caller. func (c *CachedCodeLocation) ThisCodeLocation(skiplevels ...int) *CodeLocation { var skip int if len(skiplevels) > 0 { skip = skiplevels[0] } if c == nil || !c.IsValid() { // the cache is bogus so we can't use it. return thisCodeLocationCommon(skip, false) } c.once.Do(func() { c.location = thisCodeLocationCommon(skip, true) c.err = nil }) return c.location } func removeCodeLevelMetrics(remAttr func(string)) { remAttr(AttributeCodeLineno) remAttr(AttributeCodeNamespace) remAttr(AttributeCodeFilepath) remAttr(AttributeCodeFunction) } // Evaluate a set of TraceOptions, returning a pointer to a new traceOptSet struct // initialized from those options. To avoid any unnecessary performance penalties, // if we encounter an option that suppresses CLM collection, we stop without evaluating // anything further. func resolveCLMTraceOptions(options []TraceOption) *traceOptSet { optSet := traceOptSet{} for _, o := range options { o(&optSet) if optSet.SuppressCLM { break } } return &optSet } func reportCodeLevelMetrics(tOpts traceOptSet, run *appRun, setAttr func(string, string, interface{})) { var location CodeLocation var locationp *CodeLocation if tOpts.LocationCallback != nil { locationp = tOpts.LocationCallback() } else { locationp = tOpts.LocationOverride } if locationp != nil { location = *locationp } else { pcs := make([]uintptr, 20) depth := runtime.Callers(2, pcs) if depth > 0 { frames := runtime.CallersFrames(pcs[:depth]) moreToRead := true var frame runtime.Frame if tOpts.IgnoredPrefixes == nil { tOpts.IgnoredPrefixes = run.Config.CodeLevelMetrics.IgnoredPrefixes // for backward compatibility, add the singleton IgnoredPrefix if there is one if run.Config.CodeLevelMetrics.IgnoredPrefix != "" { tOpts.IgnoredPrefixes = append(tOpts.IgnoredPrefixes, run.Config.CodeLevelMetrics.IgnoredPrefix) } if tOpts.IgnoredPrefixes == nil { tOpts.IgnoredPrefixes = append(tOpts.IgnoredPrefixes, defaultAgentProjectRoot) } } // skip out to first non-agent frame, unless that IS the top-most frame for moreToRead { frame, moreToRead = frames.Next() if func() bool { for _, eachPrefix := range tOpts.IgnoredPrefixes { if strings.HasPrefix(frame.Function, eachPrefix) { return false } } return true }() { break } } location.FilePath = frame.File location.Function = frame.Function location.LineNo = frame.Line } } if tOpts.PathPrefixes == nil { tOpts.PathPrefixes = run.Config.CodeLevelMetrics.PathPrefixes // bring in a value still lingering in the deprecated PathPrefix field if the user put one there on their own if run.Config.CodeLevelMetrics.PathPrefix != "" { tOpts.PathPrefixes = append(tOpts.PathPrefixes, run.Config.CodeLevelMetrics.PathPrefix) } } // scan for any requested suppression of leading parts of file pathnames if tOpts.PathPrefixes != nil { for _, prefix := range tOpts.PathPrefixes { if pi := strings.Index(location.FilePath, prefix); pi >= 0 { location.FilePath = location.FilePath[pi:] break } } } ns := strings.LastIndex(location.Function, ".") function := location.Function namespace := "" if ns >= 0 { namespace = location.Function[:ns] function = location.Function[ns+1:] } // Impose data value size limits. // Report no field over 255 characters in length. // Report no CLM data at all if the function name is empty or >255 chars. // Report no CLM data at all if both namespace and file path are >255 chars. if function != "" && len(function) <= 255 && (len(namespace) <= 255 || len(location.FilePath) <= 255) { setAttr(AttributeCodeLineno, "", location.LineNo) setAttr(AttributeCodeFunction, function, nil) if len(namespace) <= 255 { setAttr(AttributeCodeNamespace, namespace, nil) } if len(location.FilePath) <= 255 { setAttr(AttributeCodeFilepath, location.FilePath, nil) } } } go-agent-3.42.0/v3/newrelic/code_level_metrics_test.go000066400000000000000000000314701510742411500226630ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. package newrelic import ( "fmt" "reflect" "strings" "testing" "time" ) func anotherFunction() { time.Sleep(1 * time.Millisecond) } func TestCodeLocation(t *testing.T) { loc1 := ThisCodeLocation() if loc1.LineNo != 18 || loc1.Function != "github.com/newrelic/go-agent/v3/newrelic.TestCodeLocation" || !strings.HasSuffix(loc1.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("CodeLocation() returned %v", loc1) } loc2, err := FunctionLocation(anotherFunction) if err != nil { t.Errorf("FunctionLocation() returned error %v", err) } if loc2.LineNo != 13 || loc2.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(loc2.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation() returned %v", loc2) } } func TestBadFunctionLocation(t *testing.T) { _, err := FunctionLocation(42) if err == nil { t.Errorf("Expected error with value 42 to FunctionLocation() but got nil") } } func TestClosureCLM(t *testing.T) { l, err := FunctionLocation(func() { anotherFunction() }) if err != nil { t.Errorf("FunctionLocation of closure: %v", err) } if l.LineNo != 40 || l.Function != "github.com/newrelic/go-agent/v3/newrelic.TestClosureCLM.func1" || !strings.HasSuffix(l.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("closure FunctionLocation() returned %v", l) } } func TestBasicCaching(t *testing.T) { c := NewCachedCodeLocation() l, err := c.FunctionLocation(anotherFunction) if err != nil { t.Errorf("cached FunctionLocation error %v", err) } if l.LineNo != 13 || l.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(l.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation() returned %v", l) } if c.location == nil { t.Errorf("FunctionLocation cache location is nil") } else if c.location.LineNo != 13 || c.location.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(c.location.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation cache value is wrong %v", *c.location) } if c.Err() != nil { t.Errorf("FunctionLocation cache error %v", c.Err()) } } func TestCachedCodeLocation(t *testing.T) { c := NewCachedCodeLocation() c2 := NewCachedCodeLocation() loc1 := c.ThisCodeLocation() if loc1.LineNo != 78 || loc1.Function != "github.com/newrelic/go-agent/v3/newrelic.TestCachedCodeLocation" || !strings.HasSuffix(loc1.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("CodeLocation() returned %v", loc1) } // This should give us the previously cached value, not the new // function passed. This is actually an example of a user error in the // code since they're reusing the cache for one code location on a call // to determine the location of an entirely different function. However, // since they specified a cache that now has a value cached in it, the defined // behavior is to use the cache. loc2, err := c.FunctionLocation(anotherFunction) if err != nil { t.Errorf("FunctionLocation() returned error %v", err) } if loc2.LineNo != 78 || loc2.Function != "github.com/newrelic/go-agent/v3/newrelic.TestCachedCodeLocation" || !strings.HasSuffix(loc2.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation() returned %v", loc2) } // This is how we should have done it, using a separate cache for each // function location we're measuring. This should give us the true location loc2, err = c2.FunctionLocation(anotherFunction) if err != nil { t.Errorf("FunctionLocation() returned error %v", err) } if loc2.LineNo != 13 || loc2.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(loc2.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation() returned %v", loc2) } } func TestTraceOptions(t *testing.T) { var o traceOptSet WithCodeLocation(ThisCodeLocation())(&o) WithIgnoredPrefix("foo", "bar")(&o) WithPathPrefix("alpha", "beta", "gamma")(&o) WithoutCodeLevelMetrics()(&o) WithDefaultFunctionLocation(anotherFunction)(&o) if o.LocationOverride == nil { t.Errorf("failed to set a location") } else { if o.LocationOverride.LineNo != 110 || o.LocationOverride.Function != "github.com/newrelic/go-agent/v3/newrelic.TestTraceOptions" || !strings.HasSuffix(o.LocationOverride.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("function location set to %v", *o.LocationOverride) } } if !o.SuppressCLM { t.Errorf("asked to suppress CLM but that didn't show up") } if o.DemandCLM { t.Errorf("was not asked to demand CLM but that didn't show up") } if !reflect.DeepEqual(o.IgnoredPrefixes, []string{"foo", "bar"}) { t.Errorf("ignored prefixes wrong: %v", o.IgnoredPrefixes) } if !reflect.DeepEqual(o.PathPrefixes, []string{"alpha", "beta", "gamma"}) { t.Errorf("ignored prefixes wrong: %v", o.PathPrefixes) } } func TestTraceOptions2(t *testing.T) { var o traceOptSet WithPathPrefix("alpha")(&o) WithDefaultFunctionLocation(anotherFunction)(&o) WithCodeLevelMetrics()(&o) if o.LocationOverride == nil { t.Errorf("failed to set a location") } else { if o.LocationOverride.LineNo != 13 || o.LocationOverride.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(o.LocationOverride.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("function location set to %v", *o.LocationOverride) } } if o.SuppressCLM { t.Errorf("was not asked to suppress CLM but that didn't show up") } if !o.DemandCLM { t.Errorf("asked to demand CLM but that didn't show up") } if o.IgnoredPrefixes != nil { t.Errorf("ignored prefixes wrong: %v", o.IgnoredPrefixes) } if !reflect.DeepEqual(o.PathPrefixes, []string{"alpha"}) { t.Errorf("ignored prefixes wrong: %v", o.PathPrefixes) } } func TestNullCache(t *testing.T) { // verify that given a zero-value cache, we still fall back to the non-cached version var c CachedCodeLocation l, err := c.FunctionLocation(anotherFunction) if err != nil { t.Errorf("cached FunctionLocation error %v", err) } if l.LineNo != 13 || l.Function != "github.com/newrelic/go-agent/v3/newrelic.anotherFunction" || !strings.HasSuffix(l.FilePath, "/go-agent/v3/newrelic/code_level_metrics_test.go") { t.Errorf("FunctionLocation() returned %v", l) } if c.location != nil { t.Errorf("FunctionLocation cache location is non-nil") } if c.Err() != nil { t.Errorf("FunctionLocation cache error %v", c.Err()) } l = c.ThisCodeLocation() if l.LineNo != 193 || !strings.HasSuffix(l.Function, "TestNullCache") { t.Errorf("ThisCodeLocation line %v func %v", l.LineNo, l.Function) } } func skipA(t *testing.T) { skipB(t) } func skipB(t *testing.T) { skipC(t) } func skipC(t *testing.T) { l := ThisCodeLocation() if l.LineNo != 208 || !strings.HasSuffix(l.Function, "skipC") { t.Errorf("skipC shows as %v %v", l.LineNo, l.Function) } l = ThisCodeLocation(1) if l.LineNo != 204 || !strings.HasSuffix(l.Function, "skipB") { t.Errorf("skipB shows as %v %v", l.LineNo, l.Function) } l = ThisCodeLocation(2) if l.LineNo != 200 || !strings.HasSuffix(l.Function, "skipA") { t.Errorf("skipA shows as %v %v", l.LineNo, l.Function) } } func TestCLMSkip(t *testing.T) { skipA(t) } func skipACached(t *testing.T) { skipBCached(t) } func skipBCached(t *testing.T) { skipCCached(t) } func skipCCached(t *testing.T) { l := ThisCodeLocation() if l.LineNo != 237 || !strings.HasSuffix(l.Function, "skipCCached") { t.Errorf("skipC shows as %v %v", l.LineNo, l.Function) } l = ThisCodeLocation(1) if l.LineNo != 233 || !strings.HasSuffix(l.Function, "skipBCached") { t.Errorf("skipB shows as %v %v", l.LineNo, l.Function) } l = ThisCodeLocation(2) if l.LineNo != 229 || !strings.HasSuffix(l.Function, "skipACached") { t.Errorf("skipA shows as %v %v", l.LineNo, l.Function) } } func TestCLMSkipCached(t *testing.T) { skipACached(t) } func attributeMapMatchesCLM(expected, actual map[string]interface{}) error { for k, v := range expected { actualValue, present := actual[k] if !present { return fmt.Errorf("Expected field \"%s\" was not present in output", k) } switch value := v.(type) { case int: act, ok := actualValue.(int) if !ok { return fmt.Errorf("Expected value %v for %s was actually %v of type %T, not int", v, k, actualValue, actualValue) } if act != value { return fmt.Errorf("Expected %s value %v but got %v", k, value, act) } case string: act, ok := actualValue.(string) if !ok { return fmt.Errorf("Expected value %v for %s was actually %v of type %T, not string", v, k, actualValue, actualValue) } if act != value { return fmt.Errorf("Expected %s value %v but got %v", k, value, act) } default: return fmt.Errorf("Test case does not consider expected value %v for type %T", k, v) } } if len(expected) != len(actual) { return fmt.Errorf("expected %d fields, got %d", len(expected), len(actual)) } return nil } func TestLongCLMNames(t *testing.T) { for i, testData := range []struct { loc CodeLocation expected map[string]interface{} }{ //0 {CodeLocation{42, "main.aFunction", "/usr/local/foo.go"}, map[string]interface{}{ AttributeCodeLineno: 42, AttributeCodeFunction: "aFunction", AttributeCodeNamespace: "main", AttributeCodeFilepath: "/usr/local/foo.go", }}, //1 {CodeLocation{42, "main.aFunction", "/usr/local/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo.go"}, map[string]interface{}{ AttributeCodeLineno: 42, AttributeCodeFunction: "aFunction", AttributeCodeNamespace: "main", }}, //2 {CodeLocation{42, "main.aFunctionLoremipsumdolorsitamet.consecteturadipiscingelit.seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequat.Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariatur.Excepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", "/usr/local/foo.go"}, map[string]interface{}{ AttributeCodeLineno: 42, AttributeCodeFunction: "Excepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", AttributeCodeFilepath: "/usr/local/foo.go", }}, //3 {CodeLocation{42, "mainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", "/usr/local/foo.go"}, map[string]interface{}{}}, //4 {CodeLocation{42, "", "/usr/local/foo.go"}, map[string]interface{}{}}, //5 {CodeLocation{42, "mainmainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum.aFunction", "/usr/local/foo.go"}, map[string]interface{}{ AttributeCodeLineno: 42, AttributeCodeFunction: "aFunction", AttributeCodeFilepath: "/usr/local/foo.go", }}, //6 {CodeLocation{42, "mainmainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum.aFunction", "/usr/local/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoo.go"}, map[string]interface{}{}}, } { actual := make(map[string]interface{}) reportCodeLevelMetrics(traceOptSet{ LocationOverride: &testData.loc, PathPrefixes: []string{"xyzzy"}, }, nil, func(k, s string, v interface{}) { if v == nil { actual[k] = s } else { actual[k] = v } }) if err := attributeMapMatchesCLM(testData.expected, actual); err != nil { t.Errorf("testcase %d: %v", i, err) } } } go-agent-3.42.0/v3/newrelic/collector.go000066400000000000000000000233341510742411500177630ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "compress/gzip" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "sync" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" ) const ( // procotolVersion is the protocol version used to communicate with NR // backend. procotolVersion = 17 userAgentPrefix = "NewRelic-Go-Agent/" // Methods used in collector communication. cmdPreconnect = "preconnect" cmdConnect = "connect" cmdMetrics = "metric_data" cmdCustomEvents = "custom_event_data" cmdLogEvents = "log_event_data" cmdTxnEvents = "analytic_event_data" cmdErrorEvents = "error_event_data" cmdErrorData = "error_data" cmdTxnTraces = "transaction_sample_data" cmdSlowSQLs = "sql_trace_data" cmdSpanEvents = "span_event_data" ) // rpmCmd contains fields specific to an individual call made to RPM. type rpmCmd struct { Name string Collector string RunID string Data []byte RequestHeadersMap map[string]string MaxPayloadSize int } // rpmControls contains fields which will be the same for all calls made // by the same application. type rpmControls struct { License string Client *http.Client Logger logger.Logger GzipWriterPool *sync.Pool } // rpmResponse contains a NR endpoint response. // // Agent Behavior Summary: // // on connect/preconnect: // // 410 means shutdown // 200, 202 mean success (start run) // all other response codes and errors mean try after backoff // // on harvest: // // 410 means shutdown // 401, 409 mean restart run // 408, 429, 500, 503 mean save data for next harvest // all other response codes and errors discard the data and continue the current harvest type rpmResponse struct { statusCode int body []byte // Err indicates whether or not the call was successful: newRPMResponse // should be used to avoid mismatch between statusCode and Err. err error disconnectSecurityPolicy bool // forceSaveHarvestData overrides the status code and forces a save of data forceSaveHarvestData bool } // please create all rpmResponses this way func newRPMResponse(err error) *rpmResponse { if err == nil { return &rpmResponse{} } // remove url from errors to avoid sensitive data leaks var ue *url.Error if errors.As(err, &ue) { ue.URL = "**REDACTED-URL**" } return &rpmResponse{ err: err, } } // AddStatusCode adds an http error status code to the rpm response. This can overwrite the error // string stored in the rpm response if the code is an error code. func (resp *rpmResponse) AddStatusCode(statusCode int) *rpmResponse { resp.statusCode = statusCode if statusCode != 200 && statusCode != 202 { resp.err = fmt.Errorf("response code: %d", statusCode) } return resp } // SetError overwrites the existing response error func (resp *rpmResponse) SetError(err error) *rpmResponse { resp.err = err return resp } // AddBody adds a byte slice containing an http response body func (resp *rpmResponse) AddBody(body []byte) *rpmResponse { resp.body = body return resp } // ForceSaveHarvestData overrides the status code and forces a save of data func (resp *rpmResponse) ForceSaveHarvestData() *rpmResponse { resp.forceSaveHarvestData = true return resp } // DisconnectSecurityPolicy sets disconnectSecurityPolicy to true in the rpm response func (resp *rpmResponse) DisconnectSecurityPolicy() *rpmResponse { resp.disconnectSecurityPolicy = true return resp } // IsDisconnect indicates that the agent should disconnect. func (resp rpmResponse) IsDisconnect() bool { return resp.statusCode == 410 || resp.disconnectSecurityPolicy } // IsRestartException indicates that the agent should restart. func (resp rpmResponse) IsRestartException() bool { return resp.statusCode == 401 || resp.statusCode == 409 } func (resp rpmResponse) GetError() error { return resp.err } // ShouldSaveHarvestData indicates that the agent should save the data and try // to send it in the next harvest. func (resp rpmResponse) ShouldSaveHarvestData() bool { if resp.forceSaveHarvestData { return true } switch resp.statusCode { case 408, 429, 500, 503: return true default: return false } } func rpmURL(cmd rpmCmd, cs rpmControls) string { var u url.URL u.Host = cmd.Collector u.Path = "agent_listener/invoke_raw_method" u.Scheme = "https" query := url.Values{} query.Set("marshal_format", "json") query.Set("protocol_version", strconv.Itoa(procotolVersion)) query.Set("method", cmd.Name) query.Set("license_key", cs.License) if len(cmd.RunID) > 0 { query.Set("run_id", cmd.RunID) } u.RawQuery = query.Encode() return u.String() } func compress(b []byte, gzipWriterPool *sync.Pool) (*bytes.Buffer, error) { w := gzipWriterPool.Get().(*gzip.Writer) defer gzipWriterPool.Put(w) var buf bytes.Buffer w.Reset(&buf) _, err := w.Write(b) w.Close() if nil != err { return nil, err } return &buf, nil } func collectorRequestInternal(url string, cmd rpmCmd, cs rpmControls) *rpmResponse { compressed, err := compress(cmd.Data, cs.GzipWriterPool) if nil != err { return newRPMResponse(err) } if l := compressed.Len(); l > cmd.MaxPayloadSize { return newRPMResponse(fmt.Errorf("Payload size for %s too large: %d greater than %d", cmd.Name, l, cmd.MaxPayloadSize)) } req, err := http.NewRequest("POST", url, compressed) if nil != err { return newRPMResponse(err) } req.Header.Add("Accept-Encoding", "identity, deflate") req.Header.Add("Content-Type", "application/octet-stream") req.Header.Add("User-Agent", userAgentPrefix+Version) req.Header.Add("Content-Encoding", "gzip") for k, v := range cmd.RequestHeadersMap { req.Header.Add(k, v) } resp, err := cs.Client.Do(req) if err != nil { return newRPMResponse(err).ForceSaveHarvestData() } defer resp.Body.Close() r := newRPMResponse(nil).AddStatusCode(resp.StatusCode) // Read the entire response, rather than using resp.Body as input to json.NewDecoder to // avoid the issue described here: // https://github.com/google/go-github/pull/317 // https://ahmetalpbalkan.com/blog/golang-json-decoder-pitfalls/ // Also, collector JSON responses are expected to be quite small. body, err := io.ReadAll(resp.Body) if r.GetError() == nil { r.SetError(err) } r.AddBody(body) return r } // collectorRequest makes a request to New Relic. func collectorRequest(cmd rpmCmd, cs rpmControls) *rpmResponse { url := rpmURL(cmd, cs) urlWithoutLicense := removeLicenseFromURL(url) if cs.Logger.DebugEnabled() { cs.Logger.Debug("rpm request", map[string]interface{}{ "command": cmd.Name, "url": urlWithoutLicense, "payload": jsonString(cmd.Data), }) } resp := collectorRequestInternal(url, cmd, cs) if cs.Logger.DebugEnabled() { if err := resp.GetError(); err != nil { cs.Logger.Debug("rpm failure", map[string]interface{}{ "command": cmd.Name, "url": urlWithoutLicense, "response": string(resp.body), // Body might not be JSON on failure. "error": err.Error(), }) } else { cs.Logger.Debug("rpm response", map[string]interface{}{ "command": cmd.Name, "url": urlWithoutLicense, "response": jsonString(resp.body), }) } } return resp } func removeLicenseFromURL(u string) string { rawURL, err := url.Parse(u) if err != nil { return "" } query := rawURL.Query() licenseKey := query.Get("license_key") // License key length has already been checked, but doing another // conservative check here. if n := len(licenseKey); n > 4 { query.Set("license_key", string(licenseKey[0:2]+".."+licenseKey[n-2:])) } rawURL.RawQuery = query.Encode() return rawURL.String() } type preconnectRequest struct { SecurityPoliciesToken string `json:"security_policies_token,omitempty"` HighSecurity bool `json:"high_security"` } var ( errMissingAgentRunID = errors.New("connect reply missing agent run id") ) // connectAttempt tries to connect an application. func connectAttempt(config config, cs rpmControls) (*internal.ConnectReply, *rpmResponse) { preconnectData, err := json.Marshal([]preconnectRequest{{ SecurityPoliciesToken: config.SecurityPoliciesToken, HighSecurity: config.HighSecurity, }}) if nil != err { return nil, newRPMResponse(fmt.Errorf("unable to marshal preconnect data: %v", err)) } call := rpmCmd{ Name: cmdPreconnect, Collector: config.preconnectHost(), Data: preconnectData, MaxPayloadSize: internal.MaxPayloadSizeInBytes, } resp := collectorRequest(call, cs) if resp.GetError() != nil { return nil, resp } var preconnect struct { Preconnect internal.PreconnectReply `json:"return_value"` } err = json.Unmarshal(resp.body, &preconnect) if nil != err { resp := newRPMResponse(fmt.Errorf("unable to process preconnect reply: %v", err)) if internal.IsDisconnectSecurityPolicyError(err) { resp.DisconnectSecurityPolicy() } // Certain security policy errors must be treated as a disconnect. return nil, resp } js, err := config.createConnectJSON(preconnect.Preconnect.SecurityPolicies.PointerIfPopulated()) if nil != err { return nil, newRPMResponse(fmt.Errorf("unable to create connect data: %v", err)) } call.Collector = preconnect.Preconnect.Collector call.Data = js call.Name = cmdConnect resp = collectorRequest(call, cs) if resp.GetError() != nil { return nil, resp } reply, err := internal.UnmarshalConnectReply(resp.body, preconnect.Preconnect) if nil != err { return nil, newRPMResponse(err) } // Note: This should never happen. It would mean the collector // response is malformed. This exists merely as extra defensiveness. if "" == reply.RunID { return nil, newRPMResponse(errMissingAgentRunID) } return reply, resp } go-agent-3.42.0/v3/newrelic/collector_test.go000066400000000000000000000367471510742411500210360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "compress/gzip" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "sync" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" ) func TestURLErrorRedaction(t *testing.T) { _, err := http.Get("http://notexist.example/sensitive?sensitive=very") rpm := newRPMResponse(err) if strings.Contains(rpm.GetError().Error(), "http://notexist.example/sensitive?sensitive=very") { t.Error("Sensitive URL should have been removed from the error struct, but were not") } } func TestCollectorResponseCodeError(t *testing.T) { testcases := []struct { code int success bool disconnect bool restart bool saveHarvestData bool }{ // success {code: 200, success: true, disconnect: false, restart: false, saveHarvestData: false}, {code: 202, success: true, disconnect: false, restart: false, saveHarvestData: false}, // disconnect {code: 410, success: false, disconnect: true, restart: false, saveHarvestData: false}, // restart {code: 401, success: false, disconnect: false, restart: true, saveHarvestData: false}, {code: 409, success: false, disconnect: false, restart: true, saveHarvestData: false}, // save data {code: 408, success: false, disconnect: false, restart: false, saveHarvestData: true}, {code: 429, success: false, disconnect: false, restart: false, saveHarvestData: true}, {code: 500, success: false, disconnect: false, restart: false, saveHarvestData: true}, {code: 503, success: false, disconnect: false, restart: false, saveHarvestData: true}, // other errors {code: 400, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 403, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 404, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 405, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 407, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 411, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 413, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 414, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 415, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 417, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 431, success: false, disconnect: false, restart: false, saveHarvestData: false}, // unexpected weird codes {code: -1, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 1, success: false, disconnect: false, restart: false, saveHarvestData: false}, {code: 999999, success: false, disconnect: false, restart: false, saveHarvestData: false}, } for _, tc := range testcases { resp := newRPMResponse(nil).AddStatusCode(tc.code) if tc.success != (nil == resp.GetError()) { t.Error("error", tc.code, tc.success, resp.GetError()) } if tc.disconnect != resp.IsDisconnect() { t.Error("disconnect", tc.code, tc.disconnect, resp.GetError()) } if tc.restart != resp.IsRestartException() { t.Error("restart", tc.code, tc.restart, resp.GetError()) } if tc.saveHarvestData != resp.ShouldSaveHarvestData() { t.Error("save harvest data", tc.code, tc.saveHarvestData, resp.GetError()) } } } func TestCollectorRequest(t *testing.T) { cmd := rpmCmd{ Name: "cmd_name", Collector: "collector.com", RunID: "run_id", Data: nil, RequestHeadersMap: map[string]string{"zip": "zap"}, MaxPayloadSize: internal.MaxPayloadSizeInBytes, } testField := func(name, v1, v2 string) { if v1 != v2 { t.Error(name, v1, v2) } } cs := rpmControls{ License: "the_license", Client: &http.Client{ Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { testField("method", r.Method, "POST") testField("url", r.URL.String(), "https://collector.com/agent_listener/invoke_raw_method?license_key=the_license&marshal_format=json&method=cmd_name&protocol_version=17&run_id=run_id") testField("Accept-Encoding", r.Header.Get("Accept-Encoding"), "identity, deflate") testField("Content-Type", r.Header.Get("Content-Type"), "application/octet-stream") testField("User-Agent", r.Header.Get("User-Agent"), "NewRelic-Go-Agent/"+Version) testField("Content-Encoding", r.Header.Get("Content-Encoding"), "gzip") testField("zip", r.Header.Get("zip"), "zap") return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("body")), }, nil }), }, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } resp := collectorRequest(cmd, cs) if nil != resp.GetError() { t.Error(resp.GetError()) } } func TestCollectorBadRequest(t *testing.T) { cmd := rpmCmd{ Name: "cmd_name", Collector: "collector.com", RunID: "run_id", Data: nil, RequestHeadersMap: map[string]string{"zip": "zap"}, } cs := rpmControls{ License: "the_license", Client: &http.Client{ Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("body")), }, nil }), }, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } u := ":" // bad url resp := collectorRequestInternal(u, cmd, cs) if nil == resp.GetError() { t.Error("missing expected error") } } func TestCollectorTimeout(t *testing.T) { cmd := rpmCmd{ Name: "cmd_name", Collector: "collector.com", RunID: "run_id", Data: nil, RequestHeadersMap: map[string]string{"zip": "zap"}, MaxPayloadSize: 100, } cs := rpmControls{ License: "the_license", Client: &http.Client{ Timeout: time.Nanosecond, // force a timeout }, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } u := "https://example.com" resp := collectorRequestInternal(u, cmd, cs) if nil == resp.GetError() { t.Error("missing expected error") } if !resp.ShouldSaveHarvestData() { t.Error("harvest data should be saved when timeout occurs") } } func TestUrl(t *testing.T) { cmd := rpmCmd{ Name: "foo_method", Collector: "example.com", } cs := rpmControls{ License: "123abc", Client: nil, Logger: nil, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } out := rpmURL(cmd, cs) u, err := url.Parse(out) if err != nil { t.Fatalf("url.Parse(%q) = %q", out, err) } got := u.Query().Get("license_key") if got != cs.License { t.Errorf("got=%q cmd.License=%q", got, cs.License) } if u.Scheme != "https" { t.Error(u.Scheme) } } const ( unknownRequiredPolicyBody = `{"return_value":{"redirect_host":"special_collector","security_policies":{"unknown_policy":{"enabled":true,"required":true}}}}` redirectBody = `{"return_value":{"redirect_host":"special_collector"}}` connectBody = `{"return_value":{"agent_run_id":"my_agent_run_id"}}` malformedBody = `{"return_value":}}` ) func makeResponse(code int, body string) *http.Response { return &http.Response{ StatusCode: code, Body: io.NopCloser(strings.NewReader(body)), } } type endpointResult struct { response *http.Response err error } type connectMock struct { redirect endpointResult connect endpointResult config config } func (m connectMock) RoundTrip(r *http.Request) (*http.Response, error) { cmd := r.URL.Query().Get("method") switch cmd { case cmdPreconnect: return m.redirect.response, m.redirect.err case cmdConnect: return m.connect.response, m.connect.err default: return nil, fmt.Errorf("unknown cmd: %s", cmd) } } func (m connectMock) CancelRequest(req *http.Request) {} func testConnectHelper(cm connectMock) (*internal.ConnectReply, *rpmResponse) { cs := rpmControls{ License: "12345", Client: &http.Client{Transport: cm}, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } return connectAttempt(cm.config, cs) } func TestConnectAttemptSuccess(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(200, connectBody)}, }) if nil == run || nil != resp.GetError() { t.Fatal(run, resp.GetError()) } if run.Collector != "special_collector" { t.Error(run.Collector) } if run.RunID != "my_agent_run_id" { t.Error(run) } } func TestConnectClientError(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{err: errors.New("client error")}, }) if nil != run { t.Fatal(run) } if resp.GetError() == nil { t.Fatal("missing expected error") } } func TestConnectAttemptDisconnectOnRedirect(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(410, "")}, connect: endpointResult{response: makeResponse(200, connectBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { t.Fatal("should be disconnect") } } func TestConnectAttemptDisconnectOnConnect(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(410, "")}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { t.Fatal("should be disconnect") } } func TestConnectAttemptBadSecurityPolicies(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, unknownRequiredPolicyBody)}, connect: endpointResult{response: makeResponse(200, connectBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { t.Fatal("should be disconnect") } } func TestConnectAttemptInvalidJSON(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(200, malformedBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } } func TestConnectAttemptCollectorNotString(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, `{"return_value":123}`)}, connect: endpointResult{response: makeResponse(200, connectBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } } func TestConnectAttempt401(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(401, connectBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsRestartException() { t.Fatal("should be restart") } } func TestConnectAttemptOtherReturnCode(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(413, connectBody)}, }) if nil != run { t.Error(run) } if nil == resp.GetError() { t.Fatal("missing error") } } func TestConnectAttemptMissingRunID(t *testing.T) { run, resp := testConnectHelper(connectMock{ redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(200, `{"return_value":{}}`)}, }) if nil != run { t.Error(run) } if errMissingAgentRunID != resp.GetError() { t.Fatal("wrong error", resp.GetError()) } } func TestCollectorRequestRespectsMaxPayloadSize(t *testing.T) { // Test that CollectorRequest returns an error when MaxPayloadSize is // exceeded cmd := rpmCmd{ Name: "cmd_name", Collector: "collector.com", RunID: "run_id", Data: []byte("abcdefghijklmnopqrstuvwxyz"), MaxPayloadSize: 3, } cs := rpmControls{ Client: &http.Client{ Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { t.Error("no response should have gone out!") return nil, nil }), }, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } resp := collectorRequest(cmd, cs) if nil == resp.GetError() { t.Error("response should have contained error") } if resp.ShouldSaveHarvestData() { t.Error("harvest data should be discarded when max_payload_size_in_bytes is exceeded") } } func TestConnectReplyMaxPayloadSize(t *testing.T) { testcases := []struct { replyBody string expectedMaxPayloadSize int }{ { replyBody: `{"return_value":{"agent_run_id":"my_agent_run_id"}}`, expectedMaxPayloadSize: 1000 * 1000, }, { replyBody: `{"return_value":{"agent_run_id":"my_agent_run_id","max_payload_size_in_bytes":123}}`, expectedMaxPayloadSize: 123, }, } controls := func(replyBody string) rpmControls { return rpmControls{ Client: &http.Client{ Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader(replyBody)), }, nil }), }, Logger: logger.ShimLogger{IsDebugEnabled: true}, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, } } for _, test := range testcases { reply, resp := connectAttempt(config{}, controls(test.replyBody)) if nil != resp.GetError() { t.Error("resp returned unexpected error:", resp.GetError()) } if test.expectedMaxPayloadSize != reply.MaxPayloadSizeInBytes { t.Errorf("incorrect MaxPayloadSizeInBytes: expected=%d actual=%d", test.expectedMaxPayloadSize, reply.MaxPayloadSizeInBytes) } } } func TestPreconnectRequestMarshall(t *testing.T) { tests := map[string]preconnectRequest{ `[{"security_policies_token":"securityPoliciesToken","high_security":false}]`: { SecurityPoliciesToken: "securityPoliciesToken", HighSecurity: false, }, `[{"security_policies_token":"securityPoliciesToken","high_security":true}]`: { SecurityPoliciesToken: "securityPoliciesToken", HighSecurity: true, }, `[{"high_security":true}]`: { SecurityPoliciesToken: "", HighSecurity: true, }, `[{"high_security":false}]`: { SecurityPoliciesToken: "", HighSecurity: false, }, } for expected, request := range tests { b, e := json.Marshal([]preconnectRequest{request}) if e != nil { t.Fatal("Unable to marshall preconnect request", e) } result := string(b) if result != expected { t.Errorf("Invalid preconnect request marshall: expected %s, got %s", expected, result) } } } go-agent-3.42.0/v3/newrelic/config.go000066400000000000000000001264171510742411500172500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "errors" "fmt" "net/http" "os" "regexp" "strings" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" "github.com/newrelic/go-agent/v3/internal/sysinfo" "github.com/newrelic/go-agent/v3/internal/utilization" ) // Config contains Application and Transaction behavior settings. type Config struct { // AppName is used by New Relic to link data across servers. // // https://docs.newrelic.com/docs/apm/new-relic-apm/installation-configuration/naming-your-application AppName string // License is your New Relic license key. // // https://docs.newrelic.com/docs/accounts/install-new-relic/account-setup/license-key License string // Logger controls Go Agent logging. // // See https://github.com/newrelic/go-agent/blob/master/GUIDE.md#logging // for more examples and logging integrations. Logger Logger // Enabled controls whether the agent will communicate with the New Relic // servers and spawn goroutines. Setting this to be false is useful in // testing and staging situations. Enabled bool // Labels are key value pairs used to roll up applications into specific // categories. // // https://docs.newrelic.com/docs/using-new-relic/user-interface-functions/organize-your-data/labels-categories-organize-apps-monitors Labels map[string]string // HighSecurity guarantees that certain agent settings can not be made // more permissive. This setting must match the corresponding account // setting in the New Relic UI. // // https://docs.newrelic.com/docs/agents/manage-apm-agents/configuration/high-security-mode HighSecurity bool // SecurityPoliciesToken enables security policies if set to a non-empty // string. Only set this if security policies have been enabled on your // account. This cannot be used in conjunction with HighSecurity. // // https://docs.newrelic.com/docs/agents/manage-apm-agents/configuration/enable-configurable-security-policies SecurityPoliciesToken string // CustomInsightsEvents controls the behavior of // Application.RecordCustomEvent. // // https://docs.newrelic.com/docs/insights/new-relic-insights/adding-querying-data/inserting-custom-events-new-relic-apm-agents CustomInsightsEvents struct { // CustomAttributesEnabled Toggles whether we send our custom attributes with forwarded logs. CustomAttributesEnabled bool // CustomAttributes A hash with key/value pairs to add as custom attributes to all log events forwarded to New Relic. // https://docs.newrelic.com/docs/TODO CustomAttributesValues map[string]string // Enabled controls whether RecordCustomEvent will collect // custom analytics events. High security mode overrides this // setting. Enabled bool // MaxSamplesStored sets the desired maximum custom event samples stored MaxSamplesStored int } // TransactionEvents controls the behavior of transaction analytics // events. TransactionEvents struct { // Enabled controls whether transaction events are captured. Enabled bool // Attributes controls the attributes included with transaction // events. Attributes AttributeDestinationConfig // MaxSamplesStored allows you to limit the number of Transaction // Events stored/reported in a given 60-second period MaxSamplesStored int } // ErrorCollector controls the capture of errors. ErrorCollector struct { // Enabled controls whether errors are captured. This setting // affects both traced errors and error analytics events. Enabled bool // CaptureEvents controls whether error analytics events are // captured. CaptureEvents bool // IgnoreStatusCodes controls which http response codes are // automatically turned into errors. By default, response codes // greater than or equal to 400 or less than 100 -- with the exception // of 0, 5, and 404 -- are turned into errors. IgnoreStatusCodes []int // ExpectStatusCodes controls which http response codes should // impact your error metrics, apdex score and alerts. Expected errors will // be silently captured without impacting any of those. Note that setting an error // code as Ignored will prevent it from being collected, even if its expected. ExpectStatusCodes []int // Attributes controls the attributes included with errors. Attributes AttributeDestinationConfig // Reservoir limit for error events. Defaults to 100 MaxSamplesStored int // RecordPanics controls whether or not a deferred // Transaction.End will attempt to recover panics, record them // as errors, and then re-panic them. By default, this is // set to false. RecordPanics bool // ErrorGroupCallback is a user defined callback function that takes an error as an input // and returns a string that will be applied to an error to put it in an error group. // // If no error group is identified for a given error, this function should return an empty string. // If an ErrorGroupCallbeck is defined, it will be executed against every error the go agent notices that // is not ignored. // // example function: // // func ErrorGroupCallback(errorInfo newrelic.ErrorInfo) string { // if errorInfo.Class == "403" && errorInfo.GetUserId() == "example user" { // return "customer X payment issue" // } // // // returning empty string causes default error grouping behavior // return "" // } ErrorGroupCallback `json:"-"` } // TransactionTracer controls the capture of transaction traces. TransactionTracer struct { // Enabled controls whether transaction traces are captured. Enabled bool // Threshold controls whether a transaction trace will be // considered for capture. Of the traces exceeding the // threshold, the slowest trace every minute is captured. Threshold struct { // If IsApdexFailing is true then the trace threshold is // four times the apdex threshold. IsApdexFailing bool // If IsApdexFailing is false then this field is the // threshold, otherwise it is ignored. Duration time.Duration } // Attributes controls the attributes included with transaction // traces. Attributes AttributeDestinationConfig // Segments contains fields which control the behavior of // transaction trace segments. Segments struct { // StackTraceThreshold is the threshold at which // segments will be given a stack trace in the // transaction trace. Lowering this setting will // increase overhead. StackTraceThreshold time.Duration // Threshold is the threshold at which segments will be // added to the trace. Lowering this setting may // increase overhead. Decrease this duration if your // transaction traces are missing segments. Threshold time.Duration // Attributes controls the attributes included with each // trace segment. Attributes AttributeDestinationConfig } } // BrowserMonitoring contains settings which control the behavior of // Transaction.BrowserTimingHeader. BrowserMonitoring struct { // Enabled controls whether or not the Browser monitoring feature is // enabled. Enabled bool // Attributes controls the attributes included with Browser monitoring. // BrowserMonitoring.Attributes.Enabled is false by default, to include // attributes in the Browser timing Javascript: // // cfg.BrowserMonitoring.Attributes.Enabled = true Attributes AttributeDestinationConfig } // HostDisplayName gives this server a recognizable name in the New // Relic UI. This is an optional setting. HostDisplayName string // Transport customizes communication with the New Relic servers. This may // be used to configure a proxy. Transport http.RoundTripper // Utilization controls the detection and gathering of system // information. Utilization struct { // DetectAWS controls whether the Application attempts to detect // AWS. DetectAWS bool // DetectAzure controls whether the Application attempts to detect // Azure. DetectAzure bool // DetectPCF controls whether the Application attempts to detect // PCF. DetectPCF bool // DetectGCP controls whether the Application attempts to detect // GCP. DetectGCP bool // DetectDocker controls whether the Application attempts to // detect Docker. DetectDocker bool // DetectKubernetes controls whether the Application attempts to // detect Kubernetes. DetectKubernetes bool // These settings provide system information when custom values // are required. LogicalProcessors int TotalRAMMIB int BillingHostname string } // Heroku controls the behavior of Heroku specific features. Heroku struct { // UseDynoNames controls if Heroku dyno names are reported as the // hostname. Default is true. UseDynoNames bool // DynoNamePrefixesToShorten allows you to shorten and combine some // Heroku dyno names into a single value. Ordinarily the agent reports // dyno names with a trailing dot and process ID (for example, // worker.3). You can remove this trailing data by specifying the // prefixes you want to report without trailing data (for example, // worker.*). Defaults to shortening "scheduler" and "run" dyno names. DynoNamePrefixesToShorten []string } // AIMonitoring controls the behavior of AI monitoring features. AIMonitoring struct { Enabled bool // Indicates whether streams will be instrumented Streaming struct { Enabled bool } RecordContent struct { Enabled bool } } // CrossApplicationTracer controls behavior relating to cross application // tracing (CAT). In the case where CrossApplicationTracer and // DistributedTracer are both enabled, DistributedTracer takes precedence. // // https://docs.newrelic.com/docs/apm/transactions/cross-application-traces/introduction-cross-application-traces CrossApplicationTracer struct { Enabled bool } // DistributedTracer controls behavior relating to Distributed Tracing. In // the case where CrossApplicationTracer and DistributedTracer are both // enabled, DistributedTracer takes precedence. // // https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/introduction-distributed-tracing DistributedTracer struct { Enabled bool // ExcludeNewRelicHeader allows you to choose whether to insert the New // Relic Distributed Tracing header on outbound requests, which by // default is emitted along with the W3C trace context headers. Set // this value to true if you do not want to include the New Relic // distributed tracing header in your outbound requests. // // Disabling the New Relic header here does not prevent the agent from // accepting *inbound* New Relic headers. ExcludeNewRelicHeader bool // ReservoirLimit sets the desired maximum span event reservoir limit // for collecting span event data. The collector MAY override this value. ReservoirLimit int // Sampler controls the sampling behavior for Inbound Requests for distributed traces // If a valid traceparent exists, the following configuration options will allow // users to choose which sampling strategy to apply. // // `RemoteParentSampled` (when the traceparent sampled = 1) // * `always_on`: the agent will sample spans // * `always_off`: the agent will NOT sample spans // * `default`: the agent will use the existing NR sampling (default) // `RemoteParentNotSampled` (when the traceparent sampled = 0) // * `always_on`: the agent will sample spans // * `always_off`: the agent will NOT sample spans // * `default`: the agent will use the existing NR sampling (default) Sampler struct { RemoteParentSampled string RemoteParentNotSampled string } } // SpanEvents controls behavior relating to Span Events. Span Events // require that DistributedTracer is enabled. SpanEvents struct { Enabled bool // Attributes controls the attributes included on Spans. Attributes AttributeDestinationConfig // MaxSamplesStored allows you to limit the number of Span // Events stored/reported in a given 60-second period MaxSamplesStored int } // InfiniteTracing controls behavior related to Infinite Tracing tail based // sampling. InfiniteTracing requires that both DistributedTracer and // SpanEvents are enabled. // // https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing InfiniteTracing struct { // TraceObserver controls behavior of connecting to the Trace Observer. TraceObserver struct { // Host is the Trace Observer host to connect to and tells the // Application to enable Infinite Tracing support. When this field // is set to an empty string, which is the default, Infinite // Tracing support is disabled. Host string // Port is the Trace Observer port to connect to. The default is // 443. Port int } // SpanEvents controls the behavior of the span events sent to the // Trace Observer. SpanEvents struct { // QueueSize is the maximum number of span events that may be held // in memory as they wait to be serialized and sent to the Trace // Observer. Default value is 10,000. Any span event created when // the QueueSize limit is reached will be discarded. QueueSize int } } // DatastoreTracer controls behavior relating to datastore segments. DatastoreTracer struct { // InstanceReporting controls whether the host and port are collected // for datastore segments. InstanceReporting struct { Enabled bool } // DatabaseNameReporting controls whether the database name is // collected for datastore segments. DatabaseNameReporting struct { Enabled bool } QueryParameters struct { Enabled bool } RawQuery struct { Enabled bool } // SlowQuery controls the capture of slow query traces. Slow // query traces show you instances of your slowest datastore // segments. SlowQuery struct { Enabled bool Threshold time.Duration } } // Config Settings for Logs in Context features ApplicationLogging ApplicationLogging // Attributes controls which attributes are enabled and disabled globally. // This setting affects all attribute destinations: Transaction Events, // Error Events, Transaction Traces and segments, Traced Errors, Span // Events, and Browser timing header. Attributes AttributeDestinationConfig // RuntimeSampler controls the collection of runtime statistics like // CPU/Memory usage, goroutine count, and GC pauses. RuntimeSampler struct { // Enabled controls whether runtime statistics are captured. Enabled bool } // ServerlessMode contains fields which control behavior when running in // AWS Lambda. // // https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/introduction-new-relic-monitoring-aws-lambda ServerlessMode struct { // Enabling ServerlessMode will print each transaction's data to // stdout. No agent goroutines will be spawned in serverless mode, and // no data will be sent directly to the New Relic backend. // nrlambda.NewConfig sets Enabled to true. Enabled bool // ApdexThreshold sets the Apdex threshold when in ServerlessMode. The // default is 500 milliseconds. nrlambda.NewConfig populates this // field using the NEW_RELIC_APDEX_T environment variable. // // https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measure-user-satisfaction ApdexThreshold time.Duration // AccountID, TrustedAccountKey, and PrimaryAppID are used for // distributed tracing in ServerlessMode. AccountID and // TrustedAccountKey must be populated for distributed tracing to be // enabled. nrlambda.NewConfig populates these fields using the // NEW_RELIC_ACCOUNT_ID, NEW_RELIC_TRUSTED_ACCOUNT_KEY, and // NEW_RELIC_PRIMARY_APPLICATION_ID environment variables. AccountID string TrustedAccountKey string PrimaryAppID string } // Host can be used to override the New Relic endpoint. Host string // Error may be populated by the ConfigOptions provided to NewApplication // to indicate that setup has failed. NewApplication will return this // error if it is set. Error error // CodeLevelMetrics contains fields which control the collection and reporting // of source code context information associated with telemetry data. CodeLevelMetrics struct { // Enabling CodeLevelMetrics will include source code context information // as attributes. If this is disabled, no such metrics will be collected // or reported. Enabled bool // RedactPathPrefixes, if true, will redact a non-nil list of PathPrefixes // from the configuration data transmitted by the agent. RedactPathPrefixes bool // RedactIgnoredPrefixes, if true, will redact a non-nil list of IgnoredPrefixes // from the configuration data transmitted by the agent. RedactIgnoredPrefixes bool // Scope is a combination of CodeLevelMetricsScope values OR-ed together // to indicate which specific kinds of events will carry CodeLevelMetrics // data. This allows the agent to spend resources on discovering the source // code context data only where actually needed. Scope CodeLevelMetricsScope // PathPrefixes specifies a slice of filename patterns that describe the start of // the project area. Any text before any of these patterns is ignored. Thus, if // PathPrefixes is set to ["myproject/src", "otherproject/src"], then a function located in a file // called "/usr/local/src/myproject/src/foo.go" will be reported with the // pathname "myproject/src/foo.go". If this value is nil, the full path // will be reported (e.g., "/usr/local/src/myproject/src/foo.go"). // The first string in the slice which is found in a file pathname will be the one // used to truncate that filename; if none of the strings in PathPrefixes are found // anywhere in a file's pathname, the full path will be reported. PathPrefixes []string // PathPrefix specifies the filename pattern that describes the start of // the project area. Any text before this pattern is ignored. Thus, if // PathPrefix is set to "myproject/src", then a function located in a file // called "/usr/local/src/myproject/src/foo.go" will be reported with the // pathname "myproject/src/foo.go". If this value is empty, the full path // will be reported (e.g., "/usr/local/src/myproject/src/foo.go"). // // Deprecated: new code should use PathPrefixes instead (or better yet, // use the ConfigCodeLevelMetricsPathPrefix option, which accepts any number // of string parameters for backwards compatibility). PathPrefix string // IgnoredPrefix holds a single module path prefix to ignore when searching // to find the calling function to be reported. // // Deprecated: new code should use IgnoredPrefixes instead (or better yet, // use the ConfigCodeLevelMetricsIgnoredPrefix option, which accepts any number // of string parameters for backwards compatibility). IgnoredPrefix string // IgnoredPrefixes specifies a slice of initial patterns to look for in fully-qualified // function names to determine which functions to ignore while searching up // through the call stack to find the application function to associate // with telemetry data. The agent will look for the innermost caller whose name // does not begin with one of these prefixes. If empty, it will ignore functions whose // names look like they are internal to the agent itself. IgnoredPrefixes []string } // ModuleDependencyMetrics controls reporting of the packages used to build the instrumented // application, to help manage project dependencies. ModuleDependencyMetrics struct { // Enabled controls whether the module dependencies are collected and reported. Enabled bool // RedactIgnoredPrefixes, if true, redacts a non-nil list of IgnoredPrefixes from // the configuration data transmitted by the agent. RedactIgnoredPrefixes bool // IgnoredPrefixes is a list of module path prefixes. Any module whose import pathname // begins with one of these prefixes is excluded from the dependency reporting. // This list of ignored prefixes itself is not reported outside the agent. IgnoredPrefixes []string } // Security is used to post security configuration on UI. Security interface{} `json:"Security,omitempty"` } // CodeLevelMetricsScope is a bit-encoded value. Each such value describes // a trace type for which code-level metrics are to be collected and // reported. type CodeLevelMetricsScope uint32 // These constants specify the types of telemetry data to which we will // attach code level metric data. // // Currently, this includes // // TransactionCLM any kind of transaction // AllCLM all kinds of telemetry data for which CLM is implemented (the default) // // The zero value of CodeLevelMetricsScope means "all types" as a convenience so that // new variables of this type provide the default expected behavior // rather than, say, turning off all code level metrics as a 0 bit value would otherwise imply. // Otherwise the numeric values of these constants are not to be relied // upon and are subject to change. Only use the named constant identifiers in // your code. We do not recommend saving the raw numeric value of these constants // to use later. const ( TransactionCLM CodeLevelMetricsScope = 1 << iota AllCLM CodeLevelMetricsScope = 0 ) // CodeLevelMetricsScopeLabelToValue accepts a number of string values representing // the possible scope restrictions available for the agent, returning the // CodeLevelMetricsScope value which represents the combination of all of the given // labels. This value is suitable to be presented to ConfigCodeLevelMetricsScope. // // It also returns a boolean flag; if true, it was able to understand all of the // provided labels; otherwise, one or more of the values were not recognized and // thus the returned CodeLevelMetricsScope value may be incomplete (although it // will represent any valid label strings passed, if any). // // Currently, this function recognizes the following labels: // // for AllCLM: "all" (if this value appears anywhere in the list of strings, AllCLM will be returned) // for TransactionCLM: "transaction", "transactions", "txn" func CodeLevelMetricsScopeLabelToValue(labels ...string) (CodeLevelMetricsScope, bool) { var scope CodeLevelMetricsScope ok := true for _, label := range labels { switch label { case "": case "all": return AllCLM, true case "transaction", "transactions", "txn": scope |= TransactionCLM default: ok = false } } return scope, ok } // UnmarshalText allows for a CodeLevelMetricsScope value to be read from a JSON // string (or other text encodings) whose value is a comma-separated list of scope labels. func (s *CodeLevelMetricsScope) UnmarshalText(b []byte) error { var ok bool if *s, ok = CodeLevelMetricsScopeLabelListToValue(string(b)); !ok { return fmt.Errorf("invalid code level metrics scope label value") } return nil } // MarshalText allows for a CodeLevelMetrics value to be encoded into JSON strings and other // text encodings. func (s CodeLevelMetricsScope) MarshalText() ([]byte, error) { if s == 0 || s == AllCLM { return []byte("all"), nil } if (s & TransactionCLM) != 0 { return []byte("transaction"), nil } return nil, fmt.Errorf("unrecognized bit pattern in CodeLevelMetricsScope value") } // CodeLevelMetricsScopeLabelListToValue is a convenience function which // is like CodeLevelMetricsScopeLabeltoValue except that it takes a single // string which contains comma-separated values instead of an already-broken-out // set of individual label strings. func CodeLevelMetricsScopeLabelListToValue(labels string) (CodeLevelMetricsScope, bool) { return CodeLevelMetricsScopeLabelToValue(strings.Split(labels, ",")...) } // ApplicationLogging contains settings which control the capture and sending // of log event data type ApplicationLogging struct { // If this is disabled, all sub-features are disabled; // if it is enabled, the individual sub-feature configurations take effect. // MAY accomplish this by not installing instrumentation, or by early-return/no-op as necessary for an agent. Enabled bool // Forwarding controls log forwarding to New Relic One Forwarding struct { // Toggles whether the agent gathers log records for sending to New Relic. Enabled bool // Number of log records to send per minute to New Relic. // Controls the overall memory consumption when using log forwarding. // SHOULD be sent as part of the harvest_limits on Connect. MaxSamplesStored int Labels struct { // Toggles whether we send our labels with forwarded logs. Enabled bool // List of label types to exclude from forwarded logs. Exclude []string } } Metrics struct { // Toggles whether the agent gathers the the user facing Logging/lines and Logging/lines/{SEVERITY} // Logging Metrics used in the Logs chart on the APM Summary page. Enabled bool } LocalDecorating struct { // Toggles whether the agent enriches local logs printed to console so they can be sent to new relic for ingestion Enabled bool } // We want to enable this when your app collects fewer logs, or if your app can afford to compile the json // during log collection, slowing down the execution of the line of code that will write the log. If your // application collects logs at a high frequency or volume, or it can not afford the slowdown of marshaling objects // before sending them to new relic, we can marshal them asynchronously in the backend during harvests by setting // this to false using ConfigZapAttributesEncoder(false). ZapLogger struct { // Toggles whether zap logger field attributes are frontloaded with the zapcore.NewMapObjectEncoder or marshalled at harvest time AttributesFrontloaded bool } } // AttributeDestinationConfig controls the attributes sent to each destination. // For more information, see: // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/agent-attributes type AttributeDestinationConfig struct { // Enabled controls whether or not this destination will get any // attributes at all. For example, to prevent any attributes from being // added to errors, set: // // cfg.ErrorCollector.Attributes.Enabled = false // Enabled bool Include []string // Exclude allows you to prevent the capture of certain attributes. For // example, to prevent the capture of the request URL attribute // "request.uri", set: // // cfg.Attributes.Exclude = append(cfg.Attributes.Exclude, newrelic.AttributeRequestURI) // // The '*' character acts as a wildcard. For example, to prevent the // capture of all request related attributes, set: // // cfg.Attributes.Exclude = append(cfg.Attributes.Exclude, "request.*") // Exclude []string } // defaultConfig creates a Config populated with default settings. func defaultConfig() Config { c := Config{} c.Enabled = true c.Labels = make(map[string]string) c.CustomInsightsEvents.CustomAttributesEnabled = false c.CustomInsightsEvents.CustomAttributesValues = make(map[string]string) c.CustomInsightsEvents.Enabled = true c.CustomInsightsEvents.MaxSamplesStored = internal.MaxCustomEvents c.TransactionEvents.Enabled = true c.TransactionEvents.Attributes.Enabled = true c.TransactionEvents.MaxSamplesStored = internal.MaxTxnEvents c.HighSecurity = false c.ErrorCollector.Enabled = true c.ErrorCollector.CaptureEvents = true c.ErrorCollector.IgnoreStatusCodes = []int{ // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md 0, // gRPC OK 5, // gRPC NOT_FOUND http.StatusNotFound, // 404 } c.ErrorCollector.Attributes.Enabled = true c.ErrorCollector.MaxSamplesStored = internal.MaxErrorEvents c.Utilization.DetectAWS = true c.Utilization.DetectAzure = true c.Utilization.DetectPCF = true c.Utilization.DetectGCP = true c.Utilization.DetectDocker = true c.Utilization.DetectKubernetes = true c.Attributes.Enabled = true c.RuntimeSampler.Enabled = true c.TransactionTracer.Enabled = true c.TransactionTracer.Threshold.IsApdexFailing = true c.TransactionTracer.Threshold.Duration = 500 * time.Millisecond c.TransactionTracer.Segments.Threshold = 2 * time.Millisecond c.TransactionTracer.Segments.StackTraceThreshold = 500 * time.Millisecond c.TransactionTracer.Attributes.Enabled = true c.TransactionTracer.Segments.Attributes.Enabled = true // Application Logging Settings c.ApplicationLogging.Enabled = true c.ApplicationLogging.Forwarding.Enabled = true c.ApplicationLogging.Forwarding.MaxSamplesStored = internal.MaxLogEvents c.ApplicationLogging.Forwarding.Labels.Enabled = false c.ApplicationLogging.Forwarding.Labels.Exclude = nil c.ApplicationLogging.Metrics.Enabled = true c.ApplicationLogging.LocalDecorating.Enabled = false c.ApplicationLogging.ZapLogger.AttributesFrontloaded = true c.BrowserMonitoring.Enabled = true // browser monitoring attributes are disabled by default c.BrowserMonitoring.Attributes.Enabled = false c.CrossApplicationTracer.Enabled = false c.DistributedTracer.Enabled = true c.DistributedTracer.ReservoirLimit = internal.MaxSpanEvents c.DistributedTracer.Sampler.RemoteParentSampled = Default.String() c.DistributedTracer.Sampler.RemoteParentNotSampled = Default.String() c.SpanEvents.Enabled = true c.SpanEvents.Attributes.Enabled = true c.SpanEvents.MaxSamplesStored = internal.MaxSpanEvents c.DatastoreTracer.InstanceReporting.Enabled = true c.DatastoreTracer.DatabaseNameReporting.Enabled = true c.DatastoreTracer.QueryParameters.Enabled = true c.DatastoreTracer.SlowQuery.Enabled = true c.DatastoreTracer.SlowQuery.Threshold = 10 * time.Millisecond c.DatastoreTracer.RawQuery.Enabled = false c.ServerlessMode.ApdexThreshold = 500 * time.Millisecond c.ServerlessMode.Enabled = false c.Heroku.UseDynoNames = true c.Heroku.DynoNamePrefixesToShorten = []string{"scheduler", "run"} c.AIMonitoring.Enabled = false c.AIMonitoring.Streaming.Enabled = true c.AIMonitoring.RecordContent.Enabled = true c.InfiniteTracing.TraceObserver.Port = 443 c.InfiniteTracing.SpanEvents.QueueSize = 10000 // Code Level Metrics c.CodeLevelMetrics.Enabled = true c.CodeLevelMetrics.RedactPathPrefixes = true c.CodeLevelMetrics.RedactIgnoredPrefixes = true c.CodeLevelMetrics.Scope = AllCLM // Module Dependency Metrics c.ModuleDependencyMetrics.Enabled = true c.ModuleDependencyMetrics.RedactIgnoredPrefixes = true return c } const ( licenseLength = 40 appNameLimit = 3 ) // The following errors will be returned if your Config fails to validate. var ( errLicenseLen = fmt.Errorf("license length is not %d", licenseLength) errAppNameMissing = errors.New("string AppName required") errAppNameLimit = fmt.Errorf("max of %d rollup application names", appNameLimit) errHighSecurityWithSecurityPolicies = errors.New("SecurityPoliciesToken and HighSecurity are incompatible; please ensure HighSecurity is set to false if SecurityPoliciesToken is a non-empty string and a security policy has been set for your account") errInfTracingServerless = errors.New("ServerlessMode cannot be used with Infinite Tracing") ) // validate checks the config for improper fields. If the config is invalid, // newrelic.NewApplication returns an error. func (c Config) validate() error { if c.Enabled && !c.ServerlessMode.Enabled { if len(c.License) != licenseLength { return errLicenseLen } } else { // The License may be empty when the agent is not enabled. if len(c.License) != licenseLength && len(c.License) != 0 { return errLicenseLen } } if c.AppName == "" && c.Enabled && !c.ServerlessMode.Enabled { return errAppNameMissing } if c.HighSecurity && c.SecurityPoliciesToken != "" { return errHighSecurityWithSecurityPolicies } if strings.Count(c.AppName, ";") >= appNameLimit { return errAppNameLimit } if c.InfiniteTracing.TraceObserver.Host != "" && c.ServerlessMode.Enabled { return errInfTracingServerless } return nil } func (c Config) validateTraceObserverConfig() (*observerURL, error) { configHost := c.InfiniteTracing.TraceObserver.Host if configHost == "" { // This is the only instance from which we can return nil, nil. // If the user requests use of a trace observer, we must either provide // them with a valid observerURL _or_ alert them to the failure to do so. return nil, nil } if !versionSupports8T { return nil, errUnsupportedVersion } if !c.DistributedTracer.Enabled || !c.SpanEvents.Enabled { return nil, errSpanOrDTDisabled } return &observerURL{ host: fmt.Sprintf("%s:%d", configHost, c.InfiniteTracing.TraceObserver.Port), secure: configHost != localTestingHost, }, nil } // maxTxnEvents returns the configured maximum number of Transaction Events if it // is less than the default maximum; otherwise it returns the default max. func maxTxnEvents(configured int) int { if configured < 0 || configured > internal.MaxTxnEvents { return internal.MaxTxnEvents } return configured } // maxSpanEvents returns the configured maximum number of Span Events if it // is less than the default maximum; otherwise it returns the default max. func maxSpanEvents(configured int) int { if configured < 0 || configured > internal.MaxSpanEvents { return internal.MaxSpanEvents } return configured } // maxCustomEvents returns the configured maximum number of Custom Events if it // is less than the default maximum; otherwise it returns the default max. func maxCustomEvents(configured int) int { if configured < 0 || configured > internal.MaxCustomEvents { return internal.MaxCustomEvents } return configured } // maxErrorEvents returns the configured maximum number of Error Events if it // is less than the default maximum; otherwise it returns the default max. func maxErrorEvents(configured int) int { if configured < 0 || configured > internal.MaxErrorEvents { return internal.MaxErrorEvents } return configured } // maxLogEvents returns the configured maximum number of Log Events if it // is less than the default maximum; otherwise it returns the default max. func maxLogEvents(configured int) int { if configured < 0 || configured > internal.MaxLogEvents { return internal.MaxLogEvents } return configured } func copyDestConfig(c AttributeDestinationConfig) AttributeDestinationConfig { cp := c if nil != c.Include { cp.Include = make([]string, len(c.Include)) copy(cp.Include, c.Include) } if nil != c.Exclude { cp.Exclude = make([]string, len(c.Exclude)) copy(cp.Exclude, c.Exclude) } return cp } func copyConfigReferenceFields(cfg Config) Config { cp := cfg if nil != cfg.Labels { cp.Labels = make(map[string]string, len(cfg.Labels)) for key, val := range cfg.Labels { cp.Labels[key] = val } } if cfg.CustomInsightsEvents.CustomAttributesValues != nil { cp.CustomInsightsEvents.CustomAttributesValues = make(map[string]string, len(cfg.CustomInsightsEvents.CustomAttributesValues)) for key, val := range cfg.CustomInsightsEvents.CustomAttributesValues { cp.CustomInsightsEvents.CustomAttributesValues[key] = val } } if cfg.ErrorCollector.IgnoreStatusCodes != nil { ignored := make([]int, len(cfg.ErrorCollector.IgnoreStatusCodes)) copy(ignored, cfg.ErrorCollector.IgnoreStatusCodes) cp.ErrorCollector.IgnoreStatusCodes = ignored } cp.Attributes = copyDestConfig(cfg.Attributes) cp.ErrorCollector.Attributes = copyDestConfig(cfg.ErrorCollector.Attributes) cp.TransactionEvents.Attributes = copyDestConfig(cfg.TransactionEvents.Attributes) cp.TransactionTracer.Attributes = copyDestConfig(cfg.TransactionTracer.Attributes) cp.BrowserMonitoring.Attributes = copyDestConfig(cfg.BrowserMonitoring.Attributes) cp.SpanEvents.Attributes = copyDestConfig(cfg.SpanEvents.Attributes) cp.TransactionTracer.Segments.Attributes = copyDestConfig(cfg.TransactionTracer.Segments.Attributes) return cp } func transportSetting(t http.RoundTripper) interface{} { if nil == t { return nil } return fmt.Sprintf("%T", t) } func loggerSetting(lg Logger) interface{} { if nil == lg { return nil } if _, ok := lg.(logger.ShimLogger); ok { return nil } return fmt.Sprintf("%T", lg) } const ( // https://source.datanerd.us/agents/agent-specs/blob/master/Custom-Host-Names.md hostByteLimit = 255 ) type settings Config func (s settings) MarshalJSON() ([]byte, error) { c := Config(s) transport := c.Transport c.Transport = nil l := c.Logger c.Logger = nil js, err := json.Marshal(c) if err != nil { return nil, err } fields := make(map[string]interface{}) err = json.Unmarshal(js, &fields) if err != nil { return nil, err } // The License field is not simply ignored by adding the `json:"-"` tag // to it since we want to allow consumers to populate Config from JSON. delete(fields, `License`) fields[`Transport`] = transportSetting(transport) fields[`Logger`] = loggerSetting(l) // Browser monitoring support. if c.BrowserMonitoring.Enabled { fields[`browser_monitoring.loader`] = "rum" } // Protect privacy for restricted fields if clmConfig, ok := fields["CodeLevelMetrics"]; ok { if clmMap, ok := clmConfig.(map[string]interface{}); ok { if c.CodeLevelMetrics.RedactIgnoredPrefixes && c.CodeLevelMetrics.IgnoredPrefixes != nil { delete(clmMap, "IgnoredPrefixes") delete(clmMap, "IgnoredPrefix") } if c.CodeLevelMetrics.RedactPathPrefixes && c.CodeLevelMetrics.PathPrefixes != nil { delete(clmMap, "PathPrefixes") delete(clmMap, "PathPrefix") } } } if mdmConfig, ok := fields["ModuleDependencyMetrics"]; ok { if mdmMap, ok := mdmConfig.(map[string]interface{}); ok { if c.ModuleDependencyMetrics.RedactIgnoredPrefixes && c.ModuleDependencyMetrics.IgnoredPrefixes != nil { delete(mdmMap, "IgnoredPrefixes") } } } return json.Marshal(fields) } // labels is used for connect JSON formatting. type labels map[string]string func (l labels) MarshalJSON() ([]byte, error) { ls := make([]struct { Key string `json:"label_type"` Value string `json:"label_value"` }, len(l)) i := 0 for key, val := range l { ls[i].Key = key ls[i].Value = val i++ } return json.Marshal(ls) } func configConnectJSONInternal(c Config, pid int, util *utilization.Data, e environment, version string, securityPolicies *internal.SecurityPolicies, metadata map[string]string) ([]byte, error) { return json.Marshal([]interface{}{struct { Pid int `json:"pid"` Language string `json:"language"` Version string `json:"agent_version"` Host string `json:"host"` HostDisplayName string `json:"display_host,omitempty"` Settings interface{} `json:"settings"` AppName []string `json:"app_name"` HighSecurity bool `json:"high_security"` Labels labels `json:"labels,omitempty"` Environment environment `json:"environment"` Identifier string `json:"identifier"` Util *utilization.Data `json:"utilization"` SecurityPolicies *internal.SecurityPolicies `json:"security_policies,omitempty"` Metadata map[string]string `json:"metadata"` EventData internal.EventHarvestConfig `json:"event_harvest_config"` }{ Pid: pid, Language: agentLanguage, Version: version, Host: stringLengthByteLimit(util.Hostname, hostByteLimit), HostDisplayName: stringLengthByteLimit(c.HostDisplayName, hostByteLimit), Settings: (settings)(c), AppName: strings.Split(c.AppName, ";"), HighSecurity: c.HighSecurity, Labels: c.Labels, Environment: e, // This identifier field is provided to avoid: // https://newrelic.atlassian.net/browse/DSCORE-778 // // This identifier is used by the collector to look up the real // agent. If an identifier isn't provided, the collector will // create its own based on the first appname, which prevents a // single daemon from connecting "a;b" and "a;c" at the same // time. // // Providing the identifier below works around this issue and // allows users more flexibility in using application rollups. Identifier: c.AppName, Util: util, SecurityPolicies: securityPolicies, Metadata: metadata, EventData: internal.DefaultEventHarvestConfigWithDT(c.TransactionEvents.MaxSamplesStored, c.ApplicationLogging.Forwarding.MaxSamplesStored, c.CustomInsightsEvents.MaxSamplesStored, c.SpanEvents.MaxSamplesStored, c.DistributedTracer.Enabled), }}) } const ( // https://source.datanerd.us/agents/agent-specs/blob/master/Connect-LEGACY.md#metadata-hash metadataPrefix = "NEW_RELIC_METADATA_" ) func gatherMetadata(env []string) map[string]string { metadata := make(map[string]string) for _, pair := range env { if strings.HasPrefix(pair, metadataPrefix) { idx := strings.Index(pair, "=") if idx >= 0 { metadata[pair[0:idx]] = pair[idx+1:] } } } return metadata } // config exists to avoid adding private fields to Config. type config struct { Config // These fields based on environment variables are located here, rather // than in appRun, to ensure that they are calculated during // NewApplication (instead of at each connect) because some customers // may unset environment variables after startup: // https://github.com/newrelic/go-agent/issues/127 metadata map[string]string hostname string traceObserverURL *observerURL } func (c Config) computeDynoHostname(getenv func(string) string) string { if !c.Heroku.UseDynoNames { return "" } dyno := getenv("DYNO") if dyno == "" { return "" } for _, prefix := range c.Heroku.DynoNamePrefixesToShorten { if prefix == "" { continue } if strings.HasPrefix(dyno, prefix+".") { dyno = prefix + ".*" break } } return dyno } func newInternalConfig(cfg Config, getenv func(string) string, environ []string) (config, error) { // Copy maps and slices to prevent race conditions if a consumer changes // them after calling NewApplication. cfg = copyConfigReferenceFields(cfg) if err := cfg.validate(); nil != err { return config{}, err } obsURL, err := cfg.validateTraceObserverConfig() if err != nil { return config{}, err } // Ensure that Logger is always set to avoid nil checks. if nil == cfg.Logger { cfg.Logger = logger.ShimLogger{} } var hostname string if host := cfg.computeDynoHostname(getenv); host != "" { hostname = host } else if host, err := sysinfo.Hostname(); err == nil { hostname = host } else { hostname = "unknown" } return config{ Config: cfg, metadata: gatherMetadata(environ), hostname: hostname, traceObserverURL: obsURL, }, nil } func (c config) createConnectJSON(securityPolicies *internal.SecurityPolicies) ([]byte, error) { env := newEnvironment(&c) util := utilization.Gather(utilization.Config{ DetectAWS: c.Utilization.DetectAWS, DetectAzure: c.Utilization.DetectAzure, DetectPCF: c.Utilization.DetectPCF, DetectGCP: c.Utilization.DetectGCP, DetectDocker: c.Utilization.DetectDocker, DetectKubernetes: c.Utilization.DetectKubernetes, LogicalProcessors: c.Utilization.LogicalProcessors, TotalRAMMIB: c.Utilization.TotalRAMMIB, BillingHostname: c.Utilization.BillingHostname, Hostname: c.hostname, }, c.Logger) return configConnectJSONInternal(c.Config, os.Getpid(), util, env, Version, securityPolicies, c.metadata) } var ( preconnectHostDefault = "collector.newrelic.com" preconnectRegionLicenseRegex = regexp.MustCompile(`(^.+?)x`) ) func (c config) preconnectHost() string { if c.Host != "" { return c.Host } m := preconnectRegionLicenseRegex.FindStringSubmatch(c.License) if len(m) > 1 { return "collector." + m[1] + ".nr-data.net" } return preconnectHostDefault } go-agent-3.42.0/v3/newrelic/config_options.go000066400000000000000000001061571510742411500210220ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "fmt" "io" "maps" "os" "strconv" "strings" "unicode/utf8" ) // ConfigOption configures the Config when provided to NewApplication. type ConfigOption func(*Config) // RemoteParentSamplingConfig is an enumeration that defines the sampling behavior // for distributed tracing when a remote parent is present. It controls how the agent // decides to sample spans based on the traceparent's sampled flag. // The available options are: // - AlwaysOn: The agent will always sample spans, regardless of the traceparent's sampled flag. // - AlwaysOff: The agent will never sample spans, regardless of the traceparent's sampled flag. // - Default: The agent will use New Relic's default sampling strategy type RemoteParentSamplingConfig int const ( AlwaysOn RemoteParentSamplingConfig = iota AlwaysOff Default ) func (r RemoteParentSamplingConfig) String() string { switch r { case AlwaysOn: return "always_on" case AlwaysOff: return "always_off" case Default: return "default" default: return "default" } } // ConfigEnabled sets the whether or not the agent is enabled. func ConfigEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.Enabled = enabled } } // ConfigAppName sets the application name. func ConfigAppName(appName string) ConfigOption { return func(cfg *Config) { cfg.AppName = appName } } // ConfigLicense sets the license. func ConfigLicense(license string) ConfigOption { return func(cfg *Config) { cfg.License = license } } // ConfigDistributedTracerEnabled populates the Config's // DistributedTracer.Enabled setting. func ConfigDistributedTracerEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.DistributedTracer.Enabled = enabled } } // ConfigRemoteParentSampled sets the behavior for sampling when the remote parent is sampled. // The flag parameter is of type RemoteParentSamplingConfig, which can be: // - AlwaysOn: Sets the priority to 2 ("always_on"). // - AlwaysOff: Sets the priority to 0 ("always_off"). // - Default: Sets the priority to a value generated by New Relic. func ConfigRemoteParentSampled(flag RemoteParentSamplingConfig) ConfigOption { return func(cfg *Config) { cfg.DistributedTracer.Sampler.RemoteParentSampled = flag.String() } } // ConfigRemoteParentNotSampled sets the behavior for sampling when the remote parent is not sampled. // The flag parameter is of type RemoteParentSamplingConfig, which can be: // - AlwaysOn: Sets the priority to 2 ("always_on"). // - AlwaysOff: Sets the priority to 0 ("always_off"). // - Default: Sets the priority to a value generated by New Relic. func ConfigRemoteParentNotSampled(flag RemoteParentSamplingConfig) ConfigOption { return func(cfg *Config) { cfg.DistributedTracer.Sampler.RemoteParentNotSampled = flag.String() } } // ConfigTransactionEventsMaxSamplesStored alters the sample size allowing control // of how many transaction events are stored in an agent for a given harvest cycle. // Alters the TransactionEvents.MaxSamplesStored setting. // Note: As of Oct 2025, the absolute maximum events that can be sent each minute is 10000. func ConfigTransactionEventsMaxSamplesStored(value int) ConfigOption { return func(cfg *Config) { cfg.TransactionEvents.MaxSamplesStored = maxTxnEvents(value) } } // ConfigCustomInsightsEventsMaxSamplesStored alters the sample size allowing control // of how many custom events are stored in an agent for a given harvest cycle. // Alters the CustomInsightsEvents.MaxSamplesStored setting. // Note: As of Oct 2025, the absolute maximum events that can be sent each minute is 100000. func ConfigCustomInsightsEventsMaxSamplesStored(value int) ConfigOption { return func(cfg *Config) { cfg.CustomInsightsEvents.MaxSamplesStored = maxCustomEvents(value) } } // ConfigSpanEventsMaxSamplesStored alters the sample size allowing control // of how many span events are stored in an agent for a given harvest cycle. // Alters the SpanEvents.MaxSamplesStored setting. // Note: As of Oct 2025, the absolute maximum span events that can be sent each minute is 2000. func ConfigSpanEventsMaxSamplesStored(value int) ConfigOption { return func(cfg *Config) { cfg.SpanEvents.MaxSamplesStored = maxSpanEvents(value) } } // ConfigSpanEventsEnabled enables or disables the collection of span events. func ConfigSpanEventsEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.SpanEvents.Enabled = enabled } } // Deprecated: ConfigDistributedTracerReservoirLimit is deprecated in favor of ConfigSpanEventsMaxSamplesStored // ConfigDistributedTracerReservoirLimit alters the sample reservoir size (maximum // number of span events to be collected) for distributed tracing instead of // using the built-in default. // Alters the DistributedTracer.ReservoirLimit setting. func ConfigDistributedTracerReservoirLimit(limit int) ConfigOption { // will add some logging logic here to notify that this option is deprectated return ConfigSpanEventsMaxSamplesStored(limit) } // ConfigErrorCollectorMaxSamplesStored alters the sample size allowing control // of how many errors are stored in an agent for a given harvest cycle. // Alters the ErrorCollector.MaxSamplesStored setting. // Note: As of Oct 2025, the absolute maximum errors that can be sent each minute is 100. func ConfigErrorCollectorMaxSamplesStored(value int) ConfigOption { return func(cfg *Config) { cfg.ErrorCollector.MaxSamplesStored = maxErrorEvents(value) } } // ConfigAIMonitoringStreamingEnabled turns on or off the collection of AI Monitoring streaming mode metrics. func ConfigAIMonitoringStreamingEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.AIMonitoring.Streaming.Enabled = enabled } } // ConfigCodeLevelMetricsEnabled turns on or off the collection of code // level metrics entirely. func ConfigCodeLevelMetricsEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.Enabled = enabled } } // ConfigDatastoreRawQuery replaces a parameterized query in datastores // with the full raw query func ConfigDatastoreRawQuery(enabled bool) ConfigOption { return func(cfg *Config) { cfg.DatastoreTracer.RawQuery.Enabled = enabled } } // ConfigCodeLevelMetricsIgnoredPrefix alters the way the Code Level Metrics // collection code searches for the right function to report for a given // telemetry trace. It will find the innermost function whose name does NOT // begin with any of the strings given here. By default (or if no paramters are given), // it will ignore functions whose names imply that the function is part of // the agent itself. // // In agent version 3.18.0 (only), this took a single string parameter. // It now takes a variable number of parameters, preserving the old call semantics // for backward compatibility while allowing for multiple IgnoredPrefix values now. // // Deprecated: New code should use ConfigCodeLevelmetricsIgnoredPrefixes instead, // so the naming of this function is consistent with other related identifiers and // the fact that multiple such prefixes are now used. func ConfigCodeLevelMetricsIgnoredPrefix(prefix ...string) ConfigOption { return ConfigCodeLevelMetricsIgnoredPrefixes(prefix...) } // ConfigCodeLevelMetricsIgnoredPrefixes alters the way the Code Level Metrics // collection code searches for the right function to report for a given // telemetry trace. It will find the innermost function whose name does NOT // begin with any of the strings given here. By default (or if no paramters are given), // it will ignore functions whose names imply that the function is part of // the agent itself. func ConfigCodeLevelMetricsIgnoredPrefixes(prefix ...string) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.IgnoredPrefixes = prefix // Correct things if the user populated the old IgnoredPrefix value in the struct if cfg.CodeLevelMetrics.IgnoredPrefix != "" { cfg.CodeLevelMetrics.IgnoredPrefixes = append(cfg.CodeLevelMetrics.IgnoredPrefixes, cfg.CodeLevelMetrics.IgnoredPrefix) cfg.CodeLevelMetrics.IgnoredPrefix = "" } } } // ConfigCodeLevelMetricsRedactIgnoredPrefixes controls whether the names // of ignored modules should be redacted from the agent configuration data // reported and visible in the New Relic UI. Since one of the reasons these // modules may be excluded is to preserve confidentiality of module or // directory names, the default behavior (if this option is set to true) // is to redact those names from the configuration data so that the only thing // reported is that some list of unnamed modules were excluded from reporting. // If this is set to false, then the names of the ignored modules will be // listed in the configuration data, although those modules will still be ignored // by Code Level Metrics. func ConfigCodeLevelMetricsRedactIgnoredPrefixes(enabled bool) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.RedactIgnoredPrefixes = enabled } } // ConfigCodeLevelMetricsRedactPathPrefixes controls whether the names // of source code parent directories should be redacted from the agent configuration data // reported and visible in the New Relic UI. Since one of the reasons these // path prefixes may be excluded is to preserve confidentiality of // directory names, the default behavior (if this option is set to true) // is to redact those names from the configuration data so that the only thing // reported is that some list of unnamed path prefixes were removed from reported pathnames. // If this is set to false, then the names of the removed path prefixes will be // listed in the configuration data, although those strings will still be removed from pathnames // reported by Code Level Metrics. func ConfigCodeLevelMetricsRedactPathPrefixes(enabled bool) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.RedactPathPrefixes = enabled } } // ConfigCodeLevelMetricsScope narrows the scope of where code level // metrics are to be used. By default, if CodeLevelMetrics are enabled, // they apply everywhere the agent currently supports them. To narrow // this, supply a list of one or more CodeLevelMetricsScope values // ORed together to ConfigCodeLevelMetricsScope. // // Note that a zero value CodeLevelMetricsScope means to collect all supported // telemetry data types. If you want to stop collecting any code level metrics, // then disable collection via ConfigCodeLevelMetricsEnabled. func ConfigCodeLevelMetricsScope(scope CodeLevelMetricsScope) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.Scope = scope } } // ConfigCodeLevelMetricsPathPrefix specifies the filename pattern(s) that describe(s) the start of // the project area(s). When reporting a source filename for Code Level Metrics, and any of the // values in the path prefix list are found in the source filename, anything before that prefix // is discarded from the file pathname. This will be based on the first value in the prefix list // that is found in the pathname. // // For example, if // the path prefix list is set to ["myproject/src", "myproject/extra"], then a function located in a file // called "/usr/local/src/myproject/src/foo.go" will be reported with the // pathname "myproject/src/foo.go". If this value is empty or none of the prefix strings // are found in a file's pathname, the full path // will be reported (e.g., "/usr/local/src/myproject/src/foo.go"). // // In agent versions 3.18.0 and 3.18.1, this took a single string parameter. // It now takes a variable number of parameters, preserving the old call semantics // for backward compatibility while allowing for multiple PathPrefix values now. // // Deprecated: New code should use ConfigCodeLevelMetricsPathPrefixes instead, // so the naming of this function is consistent with other related identifiers // and the fact that multiple such prefixes are now used. func ConfigCodeLevelMetricsPathPrefix(prefix ...string) ConfigOption { return ConfigCodeLevelMetricsPathPrefixes(prefix...) } // ConfigCodeLevelMetricsPathPrefixes specifies the filename pattern(s) that describe(s) the start of // the project area(s). When reporting a source filename for Code Level Metrics, and any of the // values in the path prefix list are found in the source filename, anything before that prefix // is discarded from the file pathname. This will be based on the first value in the prefix list // that is found in the pathname. // // For example, if // the path prefix list is set to ["myproject/src", "myproject/extra"], then a function located in a file // called "/usr/local/src/myproject/src/foo.go" will be reported with the // pathname "myproject/src/foo.go". If this value is empty or none of the prefix strings // are found in a file's pathname, the full path // will be reported (e.g., "/usr/local/src/myproject/src/foo.go"). func ConfigCodeLevelMetricsPathPrefixes(prefix ...string) ConfigOption { return func(cfg *Config) { cfg.CodeLevelMetrics.PathPrefixes = prefix // Correct things if the user populated the old PathPrefix value in the struct if cfg.CodeLevelMetrics.PathPrefix != "" { cfg.CodeLevelMetrics.PathPrefixes = append(cfg.CodeLevelMetrics.PathPrefixes, cfg.CodeLevelMetrics.PathPrefix) cfg.CodeLevelMetrics.PathPrefix = "" } } } // ConfigAppLogForwardingEnabled enables or disables the collection // of logs from a user's application by the agent // Defaults: enabled=false func ConfigAppLogForwardingEnabled(enabled bool) ConfigOption { return func(cfg *Config) { if enabled { cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = true } else { cfg.ApplicationLogging.Forwarding.Enabled = false cfg.ApplicationLogging.Forwarding.MaxSamplesStored = 0 } } } // ConfigAppLogForwardingLabelsEnabled enables or disables sending our application // labels (which are configured via ConfigLabels) with forwarded log events. // Defaults: enabled=false func ConfigAppLogForwardingLabelsEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.ApplicationLogging.Forwarding.Labels.Enabled = enabled } } // ConfigAppLogForwardingLabelsExclude specifies a list of specificd label types (i.e. keys) // which should NOT be sent along with forwarded log events. func ConfigAppLogForwardingLabelsExclude(labelType ...string) ConfigOption { return func(cfg *Config) { for _, t := range labelType { t = strings.TrimSpace(t) if t != "" && !strings.ContainsAny(t, ";:") { cfg.ApplicationLogging.Forwarding.Labels.Exclude = append(cfg.ApplicationLogging.Forwarding.Labels.Exclude, t) } } } } // ConfigAppLogDecoratingEnabled enables or disables the local decoration // of logs when using one of our logs in context plugins // Defaults: enabled=false func ConfigAppLogDecoratingEnabled(enabled bool) ConfigOption { return func(cfg *Config) { if enabled { cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.LocalDecorating.Enabled = true } else { cfg.ApplicationLogging.LocalDecorating.Enabled = false } } } // ConfigAIMonitoringEnabled enables or disables the collection of AI Monitoring event data. func ConfigAIMonitoringEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.AIMonitoring.Enabled = enabled } } // ConfigAIMonitoringRecordContentEnabled enables or disables the collection of the prompt and // response data along with other AI event metadata. func ConfigAIMonitoringRecordContentEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.AIMonitoring.RecordContent.Enabled = enabled } } // ConfigAppLogMetricsEnabled enables or disables the collection of metrics // data for logs seen by an instrumented logging framework // default: true func ConfigAppLogMetricsEnabled(enabled bool) ConfigOption { return func(cfg *Config) { if enabled { cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Metrics.Enabled = true } else { cfg.ApplicationLogging.Metrics.Enabled = false } } } // ConfigAppLogEnabled enables or disables all application logging features // and data collection func ConfigAppLogEnabled(enabled bool) ConfigOption { return func(cfg *Config) { if enabled { cfg.ApplicationLogging.Enabled = true } else { cfg.ApplicationLogging.Enabled = false } } } // ConfigAppLogForwardingMaxSamplesStored allows users to set the maximium number of // log events the agent is allowed to collect and store in a given harvest cycle. // Note: As of Oct 2025, the absolute maximum log events that can be sent each minute is 10000. func ConfigAppLogForwardingMaxSamplesStored(limit int) ConfigOption { return func(cfg *Config) { cfg.ApplicationLogging.Forwarding.MaxSamplesStored = maxLogEvents(limit) } } // ConfigLogger populates the Config's Logger. func ConfigLogger(l Logger) ConfigOption { return func(cfg *Config) { cfg.Logger = l } } // ConfigInfoLogger populates the config with basic Logger at info level. func ConfigInfoLogger(w io.Writer) ConfigOption { return ConfigLogger(NewLogger(w)) } // ConfigZapAttributesEncoder controls whether the agent will frontload the zap logger field attributes with the zapcore.NewMapObjectEncoder or marshal at harvest time func ConfigZapAttributesEncoder(enabled bool) ConfigOption { return func(cfg *Config) { cfg.ApplicationLogging.ZapLogger.AttributesFrontloaded = enabled } } // ConfigModuleDependencyMetricsEnabled controls whether the agent collects and reports // the list of modules compiled into the instrumented application. func ConfigModuleDependencyMetricsEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.ModuleDependencyMetrics.Enabled = enabled } } // ConfigModuleDependencyMetricsIgnoredPrefixes sets the list of module path prefix strings // indicating which modules should be excluded from the dependency report. func ConfigModuleDependencyMetricsIgnoredPrefixes(prefix ...string) ConfigOption { return func(cfg *Config) { cfg.ModuleDependencyMetrics.IgnoredPrefixes = prefix } } // ConfigSetErrorGroupCallbackFunction set a callback function of type ErrorGroupCallback that will // be invoked against errors at harvest time. This function overrides the default grouping behavior // of errors into a custom, user defined group when set. Setting this may have performance implications // for your application depending on the contents of the callback function. Do not set this if you want // the default error grouping behavior to be executed. func ConfigSetErrorGroupCallbackFunction(callback ErrorGroupCallback) ConfigOption { return func(cfg *Config) { cfg.ErrorCollector.ErrorGroupCallback = callback } } // ConfigModuleDependencyMetricsRedactIgnoredPrefixes controls whether the names // of ignored module path prefixes should be redacted from the agent configuration data // reported and visible in the New Relic UI. Since one of the reasons these // modules may be excluded is to preserve confidentiality of module or // directory names, the default behavior (if this option is set to true) // is to redact those names from the configuration data so that the only thing // reported is that some list of unnamed modules were excluded from reporting. // If this is set to false, then the names of the ignored modules will be // listed in the configuration data, although those modules will still be ignored // by Module Dependency Metrics. func ConfigModuleDependencyMetricsRedactIgnoredPrefixes(enabled bool) ConfigOption { return func(cfg *Config) { cfg.ModuleDependencyMetrics.RedactIgnoredPrefixes = enabled } } // ConfigDebugLogger populates the config with a Logger at debug level. func ConfigDebugLogger(w io.Writer) ConfigOption { return ConfigLogger(NewDebugLogger(w)) } // ConfigLabels configures a set of labels for the application to report as attributes. // This may also be set using the NEW_RELIC_LABELS environment variable. func ConfigLabels(labels map[string]string) ConfigOption { return func(cfg *Config) { cfg.Labels = make(map[string]string) maps.Copy(cfg.Labels, labels) } } // ConfigCustomInsightsCustomAttributesEnabled enables or disables sending our application // custom attributes (which are configured via ConfigCustomInsightsCustomAttributesValues) with forwarded log events. // Defaults: enabled=false // This may also be set using the NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES_ENABLED environment variable. func ConfigCustomInsightsCustomAttributesEnabled(enabled bool) ConfigOption { return func(cfg *Config) { cfg.CustomInsightsEvents.CustomAttributesEnabled = enabled } } // ConfigCustomInsightsCustomAttributesValues configures a set of custom attributes to add as attributes to all log events forwarded to New Relic. // This may also be set using the NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES environment variable. func ConfigCustomInsightsCustomAttributesValues(customAttributes map[string]string) ConfigOption { return func(cfg *Config) { cfg.CustomInsightsEvents.CustomAttributesValues = make(map[string]string) maps.Copy(cfg.CustomInsightsEvents.CustomAttributesValues, customAttributes) } } // ConfigFromEnvironment populates the config based on environment variables: // // NEW_RELIC_APP_NAME sets AppName // NEW_RELIC_ATTRIBUTES_EXCLUDE sets Attributes.Exclude using a comma-separated list, eg. "request.headers.host,request.method" // NEW_RELIC_ATTRIBUTES_INCLUDE sets Attributes.Include using a comma-separated list // NEW_RELIC_MODULE_DEPENDENCY_METRICS_ENABLED sets ModuleDependencyMetrics.Enabled // NEW_RELIC_MODULE_DEPENDENCY_METRICS_IGNORED_PREFIXES sets ModuleDependencyMetrics.IgnoredPrefixes // NEW_RELIC_MODULE_DEPENDENCY_METRICS_REDACT_IGNORED_PREFIXES sets ModuleDependencyMetrics.RedactIgnoredPrefixes to a boolean value // NEW_RELIC_CODE_LEVEL_METRICS_ENABLED sets CodeLevelMetrics.Enabled // NEW_RELIC_CODE_LEVEL_METRICS_SCOPE sets CodeLevelMetrics.Scope using a comma-separated list, e.g. "transaction" // NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX sets CodeLevelMetrics.PathPrefixes using a comma-separated list // NEW_RELIC_CODE_LEVEL_METRICS_REDACT_PATH_PREFIXES sets CodeLevelMetrics.RedactPathPrefixes to a boolean value // NEW_RELIC_CODE_LEVEL_METRICS_REDACT_IGNORED_PREFIXES sets CodeLevelMetrics.RedactIgnoredPrefixes to a boolean value // NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX sets CodeLevelMetrics.IgnoredPrefixes using a comma-separated list // NEW_RELIC_DISTRIBUTED_TRACING_ENABLED sets DistributedTracer.Enabled using strconv.ParseBool // NEW_RELIC_ENABLED sets Enabled using strconv.ParseBool // NEW_RELIC_HIGH_SECURITY sets HighSecurity using strconv.ParseBool // NEW_RELIC_HOST sets Host // NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE sets InfiniteTracing.SpanEvents.QueueSize using strconv.Atoi // NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT sets InfiniteTracing.TraceObserver.Port using strconv.Atoi // NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST sets InfiniteTracing.TraceObserver.Host // NEW_RELIC_LABELS sets Labels using a semi-colon delimited string of colon-separated pairs, eg. "Server:One;DataCenter:Primary" // NEW_RELIC_LICENSE_KEY sets License // NEW_RELIC_LOG sets Logger to log to either "stdout" or "stderr" (filenames are not supported) // NEW_RELIC_LOG_LEVEL controls the NEW_RELIC_LOG level, must be "debug" for debug, or empty for info // NEW_RELIC_PROCESS_HOST_DISPLAY_NAME sets HostDisplayName // NEW_RELIC_SECURITY_POLICIES_TOKEN sets SecurityPoliciesToken // NEW_RELIC_UTILIZATION_BILLING_HOSTNAME sets Utilization.BillingHostname // NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS sets Utilization.LogicalProcessors using strconv.Atoi // NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB sets Utilization.TotalRAMMIB using strconv.Atoi // NEW_RELIC_APPLICATION_LOGGING_ENABLED sets ApplicationLogging.Enabled. Set to false to disable all application logging features. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED sets ApplicationLogging.LogForwarding.Enabled. Set to false to disable in agent log forwarding. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED sets ApplicationLogging.LogForwarding.Labels.Enabled to enable sending application labels with forwarded logs. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE sets ApplicationLogging.LogForwarding.Labels.Exclude to filter out a set of unwanted label types from the ones reported with logs. // NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED sets ApplicationLogging.Metrics.Enabled. Set to false to disable the collection of application log metrics. // NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED sets ApplicationLogging.LocalDecoration.Enabled. Set to true to enable local log decoration. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED sets ApplicationLogging.LogForwarding.Limit. Set to 0 to prevent captured logs from being forwarded. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES_ENABLED sets CustomInsightsEvents.CustomAttributesEnabled to enable sending application custom attributes with forwarded logs. // NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES sets CustomInsightsEvents.CustomAttributesValues A hash with key/value pairs to add as custom attributes to all log events forwarded to New Relic. // NEW_RELIC_AI_MONITORING_ENABLED sets AIMonitoring.Enabled // NEW_RELIC_AI_MONITORING_STREAMING_ENABLED sets AIMonitoring.Streaming.Enabled // NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED sets AIMonitoring.RecordContent.Enabled // // This function is strict and will assign Config.Error if any of the // environment variables cannot be parsed. func ConfigFromEnvironment() ConfigOption { return configFromEnvironment(os.Getenv) } func configFromEnvironment(getenv func(string) string) ConfigOption { return func(cfg *Config) { // Because fields could have been assigned in a previous // ConfigOption, we only want to assign fields using environment // variables that have been populated. This is especially // relevant for the string case where no processing occurs. assignBool := func(field *bool, name string) { if env := getenv(name); env != "" { if b, err := strconv.ParseBool(env); nil != err { cfg.Error = fmt.Errorf("invalid %s value: %s", name, env) } else { *field = b } } } assignInt := func(field *int, name string, fn func(configured int) int) { if env := getenv(name); env != "" { if i, err := strconv.Atoi(env); nil != err { cfg.Error = fmt.Errorf("invalid %s value: %s", name, env) } else { if fn == nil { *field = i } else { *field = fn(i) } } } } assignString := func(field *string, name string) { if env := getenv(name); env != "" { *field = env } } assignStringSlice := func(field *[]string, name string, delim string) { if env := getenv(name); env != "" { for _, part := range strings.Split(env, delim) { *field = append(*field, strings.TrimSpace(part)) } } } assignString(&cfg.AppName, "NEW_RELIC_APP_NAME") assignString(&cfg.License, "NEW_RELIC_LICENSE_KEY") assignBool(&cfg.ModuleDependencyMetrics.Enabled, "NEW_RELIC_MODULE_DEPENDENCY_METRICS_ENABLED") assignBool(&cfg.ModuleDependencyMetrics.RedactIgnoredPrefixes, "NEW_RELIC_MODULE_DEPENDENCY_METRICS_REDACT_IGNORED_PREFIXES") assignBool(&cfg.CodeLevelMetrics.Enabled, "NEW_RELIC_CODE_LEVEL_METRICS_ENABLED") assignBool(&cfg.CodeLevelMetrics.RedactPathPrefixes, "NEW_RELIC_CODE_LEVEL_METRICS_REDACT_PATH_PREFIXES") assignBool(&cfg.CodeLevelMetrics.RedactIgnoredPrefixes, "NEW_RELIC_CODE_LEVEL_METRICS_REDACT_IGNORED_PREFIXES") assignBool(&cfg.DistributedTracer.Enabled, "NEW_RELIC_DISTRIBUTED_TRACING_ENABLED") assignBool(&cfg.Enabled, "NEW_RELIC_ENABLED") assignBool(&cfg.HighSecurity, "NEW_RELIC_HIGH_SECURITY") assignString(&cfg.SecurityPoliciesToken, "NEW_RELIC_SECURITY_POLICIES_TOKEN") assignString(&cfg.Host, "NEW_RELIC_HOST") assignString(&cfg.HostDisplayName, "NEW_RELIC_PROCESS_HOST_DISPLAY_NAME") assignString(&cfg.Utilization.BillingHostname, "NEW_RELIC_UTILIZATION_BILLING_HOSTNAME") assignString(&cfg.InfiniteTracing.TraceObserver.Host, "NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST") assignInt(&cfg.InfiniteTracing.TraceObserver.Port, "NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT", nil) assignInt(&cfg.Utilization.LogicalProcessors, "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS", nil) assignInt(&cfg.Utilization.TotalRAMMIB, "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB", nil) assignInt(&cfg.InfiniteTracing.SpanEvents.QueueSize, "NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE", nil) // Application Logging Env Variables assignBool(&cfg.ApplicationLogging.Enabled, "NEW_RELIC_APPLICATION_LOGGING_ENABLED") assignBool(&cfg.ApplicationLogging.Forwarding.Enabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED") assignBool(&cfg.ApplicationLogging.Forwarding.Labels.Enabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED") assignStringSlice(&cfg.ApplicationLogging.Forwarding.Labels.Exclude, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE", ",") assignInt(&cfg.ApplicationLogging.Forwarding.MaxSamplesStored, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED", maxLogEvents) assignBool(&cfg.ApplicationLogging.Metrics.Enabled, "NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED") assignBool(&cfg.ApplicationLogging.LocalDecorating.Enabled, "NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED") assignBool(&cfg.AIMonitoring.Enabled, "NEW_RELIC_AI_MONITORING_ENABLED") assignBool(&cfg.AIMonitoring.Streaming.Enabled, "NEW_RELIC_AI_MONITORING_STREAMING_ENABLED") assignBool(&cfg.AIMonitoring.RecordContent.Enabled, "NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED") assignBool(&cfg.CustomInsightsEvents.CustomAttributesEnabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES_ENABLED") // Transaction Event Env Variables assignInt(&cfg.TransactionEvents.MaxSamplesStored, "NEW_RELIC_TRANSACTION_EVENTS_MAX_SAMPLES_STORED", maxTxnEvents) // Custom Insights Events Env Variables assignInt(&cfg.CustomInsightsEvents.MaxSamplesStored, "NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED", maxCustomEvents) // Span Event Env Variables assignBool(&cfg.SpanEvents.Enabled, "NEW_RELIC_SPAN_EVENTS_ENABLED") assignInt(&cfg.SpanEvents.MaxSamplesStored, "NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED", maxSpanEvents) // Error Collector Env Variables assignInt(&cfg.ErrorCollector.MaxSamplesStored, "NEW_RELIC_ERROR_COLLECTOR_MAX_EVENT_SAMPLES_STORED", maxErrorEvents) if env := getenv("NEW_RELIC_LABELS"); env != "" { labels, err := getLabels(getenv("NEW_RELIC_LABELS")) if err != nil { cfg.Error = fmt.Errorf("invalid NEW_RELIC_LABELS value: %s: %v", env, err) cfg.Labels = nil } else if len(labels) > 0 { cfg.Labels = labels } } if env := getenv("NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES"); env != "" { customAttributes, err := getLabels(getenv("NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES")) if err != nil { cfg.Error = fmt.Errorf("invalid NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES value: %s: %v", env, err) cfg.CustomInsightsEvents.CustomAttributesValues = nil } else if len(customAttributes) > 0 { cfg.CustomInsightsEvents.CustomAttributesValues = customAttributes } } if env := getenv("NEW_RELIC_ATTRIBUTES_INCLUDE"); env != "" { cfg.Attributes.Include = strings.Split(env, ",") } if env := getenv("NEW_RELIC_ATTRIBUTES_EXCLUDE"); env != "" { cfg.Attributes.Exclude = strings.Split(env, ",") } if env := getenv("NEW_RELIC_CODE_LEVEL_METRICS_SCOPE"); env != "" { var ok bool cfg.CodeLevelMetrics.Scope, ok = CodeLevelMetricsScopeLabelListToValue(env) if !ok { cfg.Error = fmt.Errorf("invalid NEW_RELIC_CODE_LEVEL_METRICS_SCOPE value") } } if env := getenv("NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIXES"); env != "" { cfg.CodeLevelMetrics.IgnoredPrefixes = strings.Split(env, ",") } else if env := getenv("NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX"); env != "" { cfg.CodeLevelMetrics.IgnoredPrefixes = strings.Split(env, ",") } if env := getenv("NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIXES"); env != "" { cfg.CodeLevelMetrics.PathPrefixes = strings.Split(env, ",") } else if env := getenv("NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX"); env != "" { cfg.CodeLevelMetrics.PathPrefixes = strings.Split(env, ",") } if env := getenv("NEW_RELIC_MODULE_DEPENDENCY_METRICS_IGNORED_PREFIXES"); env != "" { cfg.ModuleDependencyMetrics.IgnoredPrefixes = strings.Split(env, ",") } if env := getenv("NEW_RELIC_LOG"); env != "" { if dest := getLogDest(env); dest != nil { if isDebugEnv(getenv("NEW_RELIC_LOG_LEVEL")) { cfg.Logger = NewDebugLogger(dest) } else { cfg.Logger = NewLogger(dest) } } else { cfg.Error = fmt.Errorf("invalid NEW_RELIC_LOG value %s", env) } } } } func getLogDest(env string) io.Writer { switch env { case "stdout", "Stdout", "STDOUT": return os.Stdout case "stderr", "Stderr", "STDERR": return os.Stderr default: return nil } } func isDebugEnv(env string) bool { switch env { case "debug", "Debug", "DEBUG", "d", "D": return true default: return false } } // getLabels reads Labels from the env string, expressed as a semi-colon // delimited string of colon-separated pairs (for example, "Server:One;Data // Center:Primary"). Label keys and values must be 255 characters or less in // length. No more than 64 Labels can be set. // // This has been updated as of 3.37.0 (February 2025) to conform to newer agent // specifications by being more rigorous about what we expect and more explicitly // rejecting invalid label lists. // // We disallow (and reject the entire list of labels if any are found): // // empty key // empty value // too many delimiters in a row // not enough delimiters // // However, we silently ignore: // // leading and trailing extra semicolons // whitespace around delimiters func getLabels(env string) (map[string]string, error) { out := make(map[string]string) env = strings.Trim(env, ";\t\n\v\f\r ") for _, entry := range strings.Split(env, ";") { if entry == "" { return nil, fmt.Errorf("labels list contains empty entry") } split := strings.Split(entry, ":") if len(split) != 2 { return nil, fmt.Errorf("labels must each have \"type\":\"value\" format") } left := strings.TrimSpace(split[0]) right := strings.TrimSpace(split[1]) if left == "" || right == "" { return nil, fmt.Errorf("labels list has missing type(s) and/or value(s)") } if utf8.RuneCountInString(left) > 255 { runes := []rune(left) left = string(runes[:255]) } if utf8.RuneCountInString(right) > 255 { runes := []rune(right) right = string(runes[:255]) } // Instead of bailing out if we exceed the maximum size, we'll // just add to the output map if we still are under the allowed limit // and continue processing the input string, because there's still the // chance that we encounter an invalid string later on which would mean // we're supposed to flag it as an error and reject the whole thing. if len(out) < 64 { out[left] = right } } return out, nil } go-agent-3.42.0/v3/newrelic/config_options_test.go000066400000000000000000000446201510742411500220550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "reflect" "testing" ) func TestConfigFromEnvironment(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_APP_NAME": return "my app" case "NEW_RELIC_LICENSE_KEY": return "my license" case "NEW_RELIC_DISTRIBUTED_TRACING_ENABLED": return "true" case "NEW_RELIC_ENABLED": return "false" case "NEW_RELIC_HIGH_SECURITY": return "1" case "NEW_RELIC_SECURITY_POLICIES_TOKEN": return "my token" case "NEW_RELIC_HOST": return "my host" case "NEW_RELIC_PROCESS_HOST_DISPLAY_NAME": return "my display host" case "NEW_RELIC_UTILIZATION_BILLING_HOSTNAME": return "my billing hostname" case "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": return "123" case "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB": return "456" case "NEW_RELIC_LABELS": return "star:car;far:bar" case "NEW_RELIC_ATTRIBUTES_INCLUDE": return "zip,zap" case "NEW_RELIC_ATTRIBUTES_EXCLUDE": return "zop,zup,zep" case "NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST": return "myhost.com" case "NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT": return "456" case "NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE": return "98765" case "NEW_RELIC_CODE_LEVEL_METRICS_SCOPE": return "all" case "NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX": return "/foo/bar,/spam/spam/spam/frotz" case "NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX": return "/a/b,/c/d" case "NEW_RELIC_APPLICATION_LOGGING_ENABLED": return "false" } return "" }) expect := defaultConfig() expect.AppName = "my app" expect.License = "my license" expect.DistributedTracer.Enabled = true expect.Enabled = false expect.HighSecurity = true expect.SecurityPoliciesToken = "my token" expect.Host = "my host" expect.HostDisplayName = "my display host" expect.Utilization.BillingHostname = "my billing hostname" expect.Utilization.LogicalProcessors = 123 expect.Utilization.TotalRAMMIB = 456 expect.Labels = map[string]string{"star": "car", "far": "bar"} expect.Attributes.Include = []string{"zip", "zap"} expect.Attributes.Exclude = []string{"zop", "zup", "zep"} expect.InfiniteTracing.TraceObserver.Host = "myhost.com" expect.InfiniteTracing.TraceObserver.Port = 456 expect.InfiniteTracing.SpanEvents.QueueSize = 98765 expect.CodeLevelMetrics.Scope = AllCLM expect.CodeLevelMetrics.PathPrefixes = []string{"/foo/bar", "/spam/spam/spam/frotz"} expect.CodeLevelMetrics.IgnoredPrefixes = []string{"/a/b", "/c/d"} expect.ApplicationLogging.Enabled = false expect.ApplicationLogging.Forwarding.Enabled = true expect.ApplicationLogging.Metrics.Enabled = true expect.ApplicationLogging.LocalDecorating.Enabled = false cfg := defaultConfig() cfgOpt(&cfg) if !reflect.DeepEqual(expect, cfg) { t.Errorf("%+v", cfg) } } func TestConfigFromEnvironmentIgnoresUnset(t *testing.T) { // test that configFromEnvironment ignores unset env vars cfgOpt := configFromEnvironment(func(string) string { return "" }) cfg := defaultConfig() cfg.AppName = "something" cfg.Labels = map[string]string{"hello": "world"} cfg.Attributes.Include = []string{"zip", "zap"} cfg.Attributes.Exclude = []string{"zop", "zup", "zep"} cfg.License = "something" cfg.DistributedTracer.Enabled = true cfg.HighSecurity = true cfg.Host = "something" cfg.HostDisplayName = "something" cfg.SecurityPoliciesToken = "something" cfg.Utilization.BillingHostname = "something" cfg.Utilization.LogicalProcessors = 42 cfg.Utilization.TotalRAMMIB = 42 cfgOpt(&cfg) if cfg.AppName != "something" { t.Error("config value changed:", cfg.AppName) } if len(cfg.Labels) != 1 { t.Error("config value changed:", cfg.Labels) } if cfg.License != "something" { t.Error("config value changed:", cfg.License) } if !cfg.DistributedTracer.Enabled { t.Error("config value changed:", cfg.DistributedTracer.Enabled) } if !cfg.HighSecurity { t.Error("config value changed:", cfg.HighSecurity) } if cfg.Host != "something" { t.Error("config value changed:", cfg.Host) } if cfg.HostDisplayName != "something" { t.Error("config value changed:", cfg.HostDisplayName) } if cfg.SecurityPoliciesToken != "something" { t.Error("config value changed:", cfg.SecurityPoliciesToken) } if cfg.Utilization.BillingHostname != "something" { t.Error("config value changed:", cfg.Utilization.BillingHostname) } if cfg.Utilization.LogicalProcessors != 42 { t.Error("config value changed:", cfg.Utilization.LogicalProcessors) } if cfg.Utilization.TotalRAMMIB != 42 { t.Error("config value changed:", cfg.Utilization.TotalRAMMIB) } if len(cfg.Attributes.Include) != 2 { t.Error("config value changed:", cfg.Attributes.Include) } if len(cfg.Attributes.Exclude) != 3 { t.Error("config value changed:", cfg.Attributes.Exclude) } } func TestConfigFromEnvironmentAttributes(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_ATTRIBUTES_INCLUDE": return "zip,zap" case "NEW_RELIC_ATTRIBUTES_EXCLUDE": return "zop,zup,zep" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if !reflect.DeepEqual(cfg.Attributes.Include, []string{"zip", "zap"}) { t.Error("incorrect config value:", cfg.Attributes.Include) } if !reflect.DeepEqual(cfg.Attributes.Exclude, []string{"zop", "zup", "zep"}) { t.Error("incorrect config value:", cfg.Attributes.Exclude) } } func TestConfigFromEnvironmentInvalidBool(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_ENABLED": return "BOGUS" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if cfg.Error == nil { t.Error("error expected") } } func TestConfigFromEnvironmentInvalidInt(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS": return "BOGUS" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if cfg.Error == nil { t.Error("error expected") } } func TestConfigFromEnvironmentIntCases(t *testing.T) { tests := []struct { name string // description of the test case envVar string // environment variable name envValue string // environment variable value to set want int getValue func(*Config) int // function to extract the actual value from config }{ { name: "TraceObserver Port valid value", envVar: "NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT", envValue: "8000", want: 8000, getValue: func(c *Config) int { return c.InfiniteTracing.TraceObserver.Port }, }, { name: "Logical Processors valid value", envVar: "NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS", envValue: "123", want: 123, getValue: func(c *Config) int { return c.Utilization.LogicalProcessors }, }, { name: "Total RAM MiB valid value", envVar: "NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB", envValue: "321", want: 321, getValue: func(c *Config) int { return c.Utilization.TotalRAMMIB }, }, { name: "Span Events Queue Size valid value", envVar: "NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE", envValue: "500", want: 500, getValue: func(c *Config) int { return c.InfiniteTracing.SpanEvents.QueueSize }, }, { name: "Application Logging Forwarding Max Samples valid value", envVar: "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED", envValue: "800", want: 800, getValue: func(c *Config) int { return c.ApplicationLogging.Forwarding.MaxSamplesStored }, }, { name: "Span Events Max Samples valid value", envVar: "NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED", envValue: "2000", want: 2000, getValue: func(c *Config) int { return c.SpanEvents.MaxSamplesStored }, }, { name: "Span Events Max Samples more than maximum", envVar: "NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED", envValue: "200000", want: 2000, getValue: func(c *Config) int { return c.SpanEvents.MaxSamplesStored }, }, { name: "Span Events Max Samples less than 0", envVar: "NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED", envValue: "-1000", want: 2000, getValue: func(c *Config) int { return c.SpanEvents.MaxSamplesStored }, }, // Transaction Events test cases { name: "Transaction Events Max Samples valid value", envVar: "NEW_RELIC_TRANSACTION_EVENTS_MAX_SAMPLES_STORED", envValue: "5000", want: 5000, getValue: func(c *Config) int { return c.TransactionEvents.MaxSamplesStored }, }, { name: "Transaction Events Max Samples more than maximum", envVar: "NEW_RELIC_TRANSACTION_EVENTS_MAX_SAMPLES_STORED", envValue: "200000", want: 10000, // internal.MaxTxnEvents getValue: func(c *Config) int { return c.TransactionEvents.MaxSamplesStored }, }, { name: "Transaction Events Max Samples less than 0", envVar: "NEW_RELIC_TRANSACTION_EVENTS_MAX_SAMPLES_STORED", envValue: "-500", want: 10000, // internal.MaxTxnEvents getValue: func(c *Config) int { return c.TransactionEvents.MaxSamplesStored }, }, // Custom Insights Events test cases { name: "Custom Insights Events Max Samples valid value", envVar: "NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED", envValue: "15000", want: 15000, getValue: func(c *Config) int { return c.CustomInsightsEvents.MaxSamplesStored }, }, { name: "Custom Insights Events Max Samples more than maximum", envVar: "NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED", envValue: "500000", want: 100000, // internal.MaxCustomEvents getValue: func(c *Config) int { return c.CustomInsightsEvents.MaxSamplesStored }, }, { name: "Custom Insights Events Max Samples less than 0", envVar: "NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED", envValue: "-1500", want: 100000, // internal.MaxCustomEvents getValue: func(c *Config) int { return c.CustomInsightsEvents.MaxSamplesStored }, }, // Error Collector Events test cases { name: "Error Collector Max Event Samples valid value", envVar: "NEW_RELIC_ERROR_COLLECTOR_MAX_EVENT_SAMPLES_STORED", envValue: "50", want: 50, getValue: func(c *Config) int { return c.ErrorCollector.MaxSamplesStored }, }, { name: "Error Collector Max Event Samples more than maximum", envVar: "NEW_RELIC_ERROR_COLLECTOR_MAX_EVENT_SAMPLES_STORED", envValue: "5000", want: 100, // internal.MaxErrorEvents getValue: func(c *Config) int { return c.ErrorCollector.MaxSamplesStored }, }, { name: "Error Collector Max Event Samples less than 0", envVar: "NEW_RELIC_ERROR_COLLECTOR_MAX_EVENT_SAMPLES_STORED", envValue: "-50", want: 100, // internal.MaxErrorEvents getValue: func(c *Config) int { return c.ErrorCollector.MaxSamplesStored }, }, // Application Logging Forwarding test cases (additional beyond existing one) { name: "Application Logging Forwarding Max Samples more than maximum", envVar: "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED", envValue: "50000", want: 10000, // internal.MaxLogEvents getValue: func(c *Config) int { return c.ApplicationLogging.Forwarding.MaxSamplesStored }, }, { name: "Application Logging Forwarding Max Samples less than 0", envVar: "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED", envValue: "-800", want: 10000, // internal.MaxLogEvents getValue: func(c *Config) int { return c.ApplicationLogging.Forwarding.MaxSamplesStored }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { if s == tt.envVar { return tt.envValue } return "" }) cfg := defaultConfig() cfgOpt(&cfg) got := tt.getValue(&cfg) if got != tt.want { t.Errorf("got %s = %d, want %s = %d", tt.envVar, got, tt.envVar, tt.want) } }) } } func TestConfigFromEnvironmentInvalidLogger(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_LOG": return "BOGUS" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if cfg.Error == nil { t.Error("error expected") } } func TestConfigFromEnvironmentInvalidLabels(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_LABELS": return ";;;" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if cfg.Error == nil { t.Error("error expected") } } func TestConfigFromEnvironmentLabelsSuccess(t *testing.T) { cfgOpt := configFromEnvironment(func(s string) string { switch s { case "NEW_RELIC_LABELS": return "zip:zap; zop:zup" default: return "" } }) cfg := defaultConfig() cfgOpt(&cfg) if !reflect.DeepEqual(cfg.Labels, map[string]string{"zip": "zap", "zop": "zup"}) { t.Error(cfg.Labels) } } func TestConfigRemoteParentSamplingDefaults(t *testing.T) { cfg := defaultConfig() if cfg.DistributedTracer.Sampler.RemoteParentNotSampled != "default" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentNotSampled:", cfg.DistributedTracer.Sampler.RemoteParentNotSampled) } if cfg.DistributedTracer.Sampler.RemoteParentSampled != "default" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentSampled:", cfg.DistributedTracer.Sampler.RemoteParentSampled) } } func TestConfigRemoteParentSampledOn(t *testing.T) { cfgOpt := ConfigRemoteParentSampled(AlwaysOn) cfg := defaultConfig() cfgOpt(&cfg) if cfg.DistributedTracer.Sampler.RemoteParentSampled != "always_on" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentSampled:", cfg.DistributedTracer.Sampler.RemoteParentSampled) } } func TestConfigRemoteParentSampledOff(t *testing.T) { cfgOpt := ConfigRemoteParentSampled(AlwaysOff) cfg := defaultConfig() cfgOpt(&cfg) if cfg.DistributedTracer.Sampler.RemoteParentSampled != "always_off" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentSampled:", cfg.DistributedTracer.Sampler.RemoteParentSampled) } } func TestConfigRemoteParentNotSampledOn(t *testing.T) { cfgOpt := ConfigRemoteParentNotSampled(AlwaysOn) cfg := defaultConfig() cfgOpt(&cfg) if cfg.DistributedTracer.Sampler.RemoteParentNotSampled != "always_on" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentNotSampled:", cfg.DistributedTracer.Sampler.RemoteParentNotSampled) } } func TestConfigRemoteParentNotSampledOff(t *testing.T) { cfgOpt := ConfigRemoteParentNotSampled(AlwaysOff) cfg := defaultConfig() cfgOpt(&cfg) if cfg.DistributedTracer.Sampler.RemoteParentNotSampled != "always_off" { t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentNotSampled:", cfg.DistributedTracer.Sampler.RemoteParentNotSampled) } } func TestConfigEventsMaxSamplesStored(t *testing.T) { // Test all event types with their MaxSamplesStored configuration eventTypes := []struct { name string maxLimit int configFunc func(int) ConfigOption // specific function that we are testing getConfigValue func(*Config) int // specific config value we are setting }{ { name: "SpanEvents", maxLimit: 2000, // internal.MaxSpanEvents configFunc: ConfigSpanEventsMaxSamplesStored, getConfigValue: func(c *Config) int { return c.SpanEvents.MaxSamplesStored }, }, { name: "SpanEvents (deprecated) distributed tracer reservoir limit", maxLimit: 2000, // internal.MaxSpanEvents configFunc: ConfigDistributedTracerReservoirLimit, getConfigValue: func(c *Config) int { return c.SpanEvents.MaxSamplesStored }, }, { name: "TransactionEvents", maxLimit: 10000, // internal.MaxTxnEvents configFunc: ConfigTransactionEventsMaxSamplesStored, getConfigValue: func(c *Config) int { return c.TransactionEvents.MaxSamplesStored }, }, { name: "CustomInsightsEvents", maxLimit: 100000, // internal.MaxCustomEvents configFunc: ConfigCustomInsightsEventsMaxSamplesStored, getConfigValue: func(c *Config) int { return c.CustomInsightsEvents.MaxSamplesStored }, }, { name: "ErrorCollector", maxLimit: 100, // internal.MaxErrorEvents configFunc: ConfigErrorCollectorMaxSamplesStored, getConfigValue: func(c *Config) int { return c.ErrorCollector.MaxSamplesStored }, }, { name: "ApplicationLogging", maxLimit: 10000, // internal.MaxLogEvents configFunc: ConfigAppLogForwardingMaxSamplesStored, getConfigValue: func(c *Config) int { return c.ApplicationLogging.Forwarding.MaxSamplesStored }, }, } // Test cases common to all event types testCases := []struct { name string getLimit func(maxLimit int) int // function to get event function parameter getWant func(maxLimit int) int // function to get expected value }{ { name: "MaxSamplesStored is less than 0", getLimit: func(maxLimit int) int { return -1 }, getWant: func(maxLimit int) int { return maxLimit }, }, { name: "MaxSamplesStored is greater than max", getLimit: func(maxLimit int) int { return maxLimit + 1 }, getWant: func(maxLimit int) int { return maxLimit }, }, { name: "MaxSamplesStored is much greater than max", getLimit: func(maxLimit int) int { return maxLimit * 50 }, getWant: func(maxLimit int) int { return maxLimit }, }, { name: "MaxSamplesStored is between 0 and max", getLimit: func(maxLimit int) int { return maxLimit / 2 }, getWant: func(maxLimit int) int { return maxLimit / 2 }, }, { name: "MaxSamplesStored is 0", getLimit: func(maxLimit int) int { return 0 }, getWant: func(maxLimit int) int { return 0 }, // right now all the functions return 0. This can be changed to a zeroValue in the eventTypes struct }, { name: "MaxSamplesStored is equal to max", getLimit: func(maxLimit int) int { return maxLimit }, getWant: func(maxLimit int) int { return maxLimit }, }, } for _, eventType := range eventTypes { t.Run(eventType.name, func(t *testing.T) { for _, tt := range testCases { limit := tt.getLimit(eventType.maxLimit) // want := tt.getWant(eventType.maxLimit) t.Run(tt.name, func(t *testing.T) { cfgOpt := eventType.configFunc(limit) cfg := defaultConfig() cfgOpt(&cfg) got := eventType.getConfigValue(&cfg) if got != want { t.Errorf("%s.MaxSamplesStored = %v, want %v", eventType.name, got, want) } }) } }) } } go-agent-3.42.0/v3/newrelic/config_test.go000066400000000000000000000767571510742411500203220ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "net/http" "os" "reflect" "regexp" "strconv" "strings" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/crossagent" "github.com/newrelic/go-agent/v3/internal/utilization" ) type labelsTestCase struct { Name string `json:"name"` LabelString string `json:"labelString"` Warning bool `json:"warning"` Expected []struct { LabelType string `json:"label_type"` LabelValue string `json:"label_value"` } `json:"expected"` } func TestCrossAgentLabels(t *testing.T) { var tcs []json.RawMessage err := crossagent.ReadJSON("labels.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { runLabelsTestCase(t, tc) } } func runLabelsTestCase(t *testing.T, js json.RawMessage) { var tc labelsTestCase if err := json.Unmarshal(js, &tc); nil != err { t.Error(err) return } actual, _ := getLabels(tc.LabelString) if len(actual) != len(tc.Expected) { t.Errorf("%s: incorrect number of elements: actual=%d expect=%d", tc.Name, len(actual), len(tc.Expected)) return } for _, exp := range tc.Expected { if v, ok := actual[exp.LabelType]; !ok { t.Errorf("%s: key %s not in actual: actual=%#v", tc.Name, exp.LabelType, actual) } else if v != exp.LabelValue { t.Errorf("%s: incorrect value found for key %s: actual=%#v expect=%#v", tc.Name, exp.LabelType, actual, tc.Expected) } } } var ( fixRegex = regexp.MustCompile(`e\+\d+`) ) // In Go 1.8 Marshalling of numbers was changed: // Before: "StackTraceThreshold":5e+08 // After: "StackTraceThreshold":500000000 func standardizeNumbers(input string) string { return fixRegex.ReplaceAllStringFunc(input, func(s string) string { n, err := strconv.Atoi(s[2:]) if nil != err { return s } return strings.Repeat("0", n) }) } func TestCopyConfigReferenceFieldsPresent(t *testing.T) { cfg := defaultConfig() cfg.AppName = "my appname" cfg.License = "0123456789012345678901234567890123456789" cfg.Labels["zip"] = "zap" cfg.CustomInsightsEvents.CustomAttributesValues["fiz"] = "baz" cfg.ErrorCollector.IgnoreStatusCodes = append(cfg.ErrorCollector.IgnoreStatusCodes, 405) cfg.ErrorCollector.ExpectStatusCodes = append(cfg.ErrorCollector.ExpectStatusCodes, 500) cfg.Attributes.Include = append(cfg.Attributes.Include, "1") cfg.Attributes.Exclude = append(cfg.Attributes.Exclude, "2") cfg.TransactionEvents.Attributes.Include = append(cfg.TransactionEvents.Attributes.Include, "3") cfg.TransactionEvents.Attributes.Exclude = append(cfg.TransactionEvents.Attributes.Exclude, "4") cfg.ErrorCollector.Attributes.Include = append(cfg.ErrorCollector.Attributes.Include, "5") cfg.ErrorCollector.Attributes.Exclude = append(cfg.ErrorCollector.Attributes.Exclude, "6") cfg.TransactionTracer.Attributes.Include = append(cfg.TransactionTracer.Attributes.Include, "7") cfg.TransactionTracer.Attributes.Exclude = append(cfg.TransactionTracer.Attributes.Exclude, "8") cfg.BrowserMonitoring.Attributes.Include = append(cfg.BrowserMonitoring.Attributes.Include, "9") cfg.BrowserMonitoring.Attributes.Exclude = append(cfg.BrowserMonitoring.Attributes.Exclude, "10") cfg.SpanEvents.Attributes.Include = append(cfg.SpanEvents.Attributes.Include, "11") cfg.SpanEvents.Attributes.Exclude = append(cfg.SpanEvents.Attributes.Exclude, "12") cfg.TransactionTracer.Segments.Attributes.Include = append(cfg.TransactionTracer.Segments.Attributes.Include, "13") cfg.TransactionTracer.Segments.Attributes.Exclude = append(cfg.TransactionTracer.Segments.Attributes.Exclude, "14") cfg.Transport = &http.Transport{} cfg.Logger = NewLogger(os.Stdout) cp := copyConfigReferenceFields(cfg) cfg.Labels["zop"] = "zup" cfg.Labels["foz"] = "buz" cfg.ErrorCollector.IgnoreStatusCodes[0] = 201 cfg.Attributes.Include[0] = "zap" cfg.Attributes.Exclude[0] = "zap" cfg.TransactionEvents.Attributes.Include[0] = "zap" cfg.TransactionEvents.Attributes.Exclude[0] = "zap" cfg.ErrorCollector.Attributes.Include[0] = "zap" cfg.ErrorCollector.Attributes.Exclude[0] = "zap" cfg.TransactionTracer.Attributes.Include[0] = "zap" cfg.TransactionTracer.Attributes.Exclude[0] = "zap" cfg.BrowserMonitoring.Attributes.Include[0] = "zap" cfg.BrowserMonitoring.Attributes.Exclude[0] = "zap" cfg.SpanEvents.Attributes.Include[0] = "zap" cfg.SpanEvents.Attributes.Exclude[0] = "zap" cfg.TransactionTracer.Segments.Attributes.Include[0] = "zap" cfg.TransactionTracer.Segments.Attributes.Exclude[0] = "zap" expect := internal.CompactJSONString(fmt.Sprintf(`[ { "pid":123, "language":"go", "agent_version":"0.2.2", "host":"my-hostname", "settings":{ "AIMonitoring": { "Enabled": false, "RecordContent": { "Enabled": true }, "Streaming": { "Enabled": true } }, "AppName":"my appname", "ApplicationLogging": { "Enabled": true, "Forwarding": { "Enabled": true, "Labels": { "Enabled": false, "Exclude": null }, "MaxSamplesStored": %d }, "LocalDecorating":{ "Enabled": false }, "Metrics": { "Enabled": true }, "ZapLogger": { "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":["2"],"Include":["1"]}, "BrowserMonitoring":{ "Attributes":{"Enabled":false,"Exclude":["10"],"Include":["9"]}, "Enabled":true }, "CodeLevelMetrics":{"Enabled":true,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, "CrossApplicationTracer":{"Enabled":false}, "CustomInsightsEvents":{ "CustomAttributesEnabled":false, "CustomAttributesValues":{"fiz":"baz"}, "Enabled":true, "MaxSamplesStored":%d }, "DatastoreTracer":{ "DatabaseNameReporting":{"Enabled":true}, "InstanceReporting":{"Enabled":true}, "QueryParameters":{"Enabled":true}, "RawQuery":{"Enabled":false}, "SlowQuery":{ "Enabled":true, "Threshold":10000000 } }, "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":%d,"Sampler":{"RemoteParentNotSampled":"default","RemoteParentSampled":"default"}}, "Enabled":true, "Error":null, "ErrorCollector":{ "Attributes":{"Enabled":true,"Exclude":["6"],"Include":["5"]}, "CaptureEvents":true, "Enabled":true, "ExpectStatusCodes":[500], "IgnoreStatusCodes":[0,5,404,405], "MaxSamplesStored": %d, "RecordPanics":false }, "Heroku":{ "DynoNamePrefixesToShorten":["scheduler","run"], "UseDynoNames":true }, "HighSecurity":false, "Host":"", "HostDisplayName":"", "InfiniteTracing": { "SpanEvents": {"QueueSize":10000}, "TraceObserver": { "Host": "", "Port": 443 } }, "Labels":{"zip":"zap"}, "Logger":"*logger.logFile", "ModuleDependencyMetrics":{"Enabled":true,"IgnoredPrefixes":null,"RedactIgnoredPrefixes":true}, "RuntimeSampler":{"Enabled":true}, "SecurityPoliciesToken":"", "ServerlessMode":{ "AccountID":"", "ApdexThreshold":500000000, "Enabled":false, "PrimaryAppID":"", "TrustedAccountKey":"" }, "SpanEvents":{ "Attributes":{ "Enabled":true,"Exclude":["12"],"Include":["11"] }, "Enabled":true, "MaxSamplesStored": %d }, "TransactionEvents":{ "Attributes":{"Enabled":true,"Exclude":["4"],"Include":["3"]}, "Enabled":true, "MaxSamplesStored": %d }, "TransactionTracer":{ "Attributes":{"Enabled":true,"Exclude":["8"],"Include":["7"]}, "Enabled":true, "Segments":{ "Attributes":{"Enabled":true,"Exclude":["14"],"Include":["13"]}, "StackTraceThreshold":500000000, "Threshold":2000000 }, "Threshold":{ "Duration":500000000, "IsApdexFailing":true } }, "Transport":"*http.Transport", "Utilization":{ "BillingHostname":"", "DetectAWS":true, "DetectAzure":true, "DetectDocker":true, "DetectGCP":true, "DetectKubernetes":true, "DetectPCF":true, "LogicalProcessors":0, "TotalRAMMIB":0 }, "browser_monitoring.loader":"rum" }, "app_name":["my appname"], "high_security":false, "labels":[{"label_type":"zip","label_value":"zap"}], "environment":[ ["runtime.NumCPU",8], ["runtime.Compiler","comp"], ["runtime.GOARCH","arch"], ["runtime.GOOS","goos"], ["runtime.Version","vers"], ["Modules", null] ], "identifier":"my appname", "utilization":{ "metadata_version":5, "logical_processors":16, "total_ram_mib":1024, "hostname":"my-hostname" }, "security_policies":{ "record_sql":{"enabled":false}, "attributes_include":{"enabled":false}, "allow_raw_exception_messages":{"enabled":false}, "custom_events":{"enabled":false}, "custom_parameters":{"enabled":false} }, "metadata":{ "NEW_RELIC_METADATA_ZAP":"zip" }, "event_harvest_config": { "report_period_ms": 60000, "harvest_limits": { "analytic_event_data": 10000, "custom_event_data": %d, "log_event_data": %d, "error_event_data": 100, "span_event_data": %d } } }]`, internal.MaxLogEvents, internal.MaxCustomEvents, internal.MaxSpanEvents, internal.MaxErrorEvents, internal.MaxSpanEvents, internal.MaxTxnEvents, internal.MaxCustomEvents, internal.MaxTxnEvents, internal.MaxSpanEvents)) securityPoliciesInput := []byte(`{ "record_sql": { "enabled": false, "required": false }, "attributes_include": { "enabled": false, "required": false }, "allow_raw_exception_messages": { "enabled": false, "required": false }, "custom_events": { "enabled": false, "required": false }, "custom_parameters": { "enabled": false, "required": false }, "custom_instrumentation_editor": { "enabled": false, "required": false }, "message_parameters": { "enabled": false, "required": false }, "job_arguments": { "enabled": false, "required": false } }`) var sp internal.SecurityPolicies err := json.Unmarshal(securityPoliciesInput, &sp) if err != nil { t.Fatal(err) } metadata := map[string]string{ "NEW_RELIC_METADATA_ZAP": "zip", } js, err := configConnectJSONInternal(cp, 123, &utilization.SampleData, sampleEnvironment, "0.2.2", sp.PointerIfPopulated(), metadata) if err != nil { t.Fatal(err) } out := standardizeNumbers(string(js)) if out != expect { t.Error(expect) t.Error(out) } } func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { cfg := defaultConfig() cfg.AppName = "my appname" cfg.CustomInsightsEvents.CustomAttributesValues = nil cfg.License = "0123456789012345678901234567890123456789" cfg.Labels = nil cfg.ErrorCollector.IgnoreStatusCodes = nil cp := copyConfigReferenceFields(cfg) expect := internal.CompactJSONString(fmt.Sprintf(`[ { "pid":123, "language":"go", "agent_version":"0.2.2", "host":"my-hostname", "settings":{ "AIMonitoring": { "Enabled": false, "RecordContent": { "Enabled": true }, "Streaming": { "Enabled": true } }, "AppName":"my appname", "ApplicationLogging": { "Enabled": true, "Forwarding": { "Enabled": true, "Labels": { "Enabled": false, "Exclude": null }, "MaxSamplesStored": %d }, "LocalDecorating":{ "Enabled": false }, "Metrics": { "Enabled": true }, "ZapLogger": { "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "BrowserMonitoring":{ "Attributes":{ "Enabled":false, "Exclude":null, "Include":null }, "Enabled":true }, "CodeLevelMetrics":{"Enabled":true,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, "CrossApplicationTracer":{"Enabled":false}, "CustomInsightsEvents":{ "CustomAttributesEnabled": false, "CustomAttributesValues": null, "Enabled":true, "MaxSamplesStored":%d }, "DatastoreTracer":{ "DatabaseNameReporting":{"Enabled":true}, "InstanceReporting":{"Enabled":true}, "QueryParameters":{"Enabled":true}, "RawQuery":{"Enabled":false}, "SlowQuery":{ "Enabled":true, "Threshold":10000000 } }, "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":%d,"Sampler":{"RemoteParentNotSampled":"default","RemoteParentSampled":"default"}}, "Enabled":true, "Error":null, "ErrorCollector":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "CaptureEvents":true, "Enabled":true, "ExpectStatusCodes":null, "IgnoreStatusCodes":null, "MaxSamplesStored": %d, "RecordPanics":false }, "Heroku":{ "DynoNamePrefixesToShorten":["scheduler","run"], "UseDynoNames":true }, "HighSecurity":false, "Host":"", "HostDisplayName":"", "InfiniteTracing": { "SpanEvents": {"QueueSize":10000}, "TraceObserver": { "Host": "", "Port": 443 } }, "Labels":null, "Logger":null, "ModuleDependencyMetrics":{"Enabled":true,"IgnoredPrefixes":null,"RedactIgnoredPrefixes":true}, "RuntimeSampler":{"Enabled":true}, "SecurityPoliciesToken":"", "ServerlessMode":{ "AccountID":"", "ApdexThreshold":500000000, "Enabled":false, "PrimaryAppID":"", "TrustedAccountKey":"" }, "SpanEvents":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "Enabled":true, "MaxSamplesStored": %d }, "TransactionEvents":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "Enabled":true, "MaxSamplesStored": %d }, "TransactionTracer":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "Enabled":true, "Segments":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "StackTraceThreshold":500000000, "Threshold":2000000 }, "Threshold":{ "Duration":500000000, "IsApdexFailing":true } }, "Transport":null, "Utilization":{ "BillingHostname":"", "DetectAWS":true, "DetectAzure":true, "DetectDocker":true, "DetectGCP":true, "DetectKubernetes":true, "DetectPCF":true, "LogicalProcessors":0, "TotalRAMMIB":0 }, "browser_monitoring.loader":"rum" }, "app_name":["my appname"], "high_security":false, "environment":[ ["runtime.NumCPU",8], ["runtime.Compiler","comp"], ["runtime.GOARCH","arch"], ["runtime.GOOS","goos"], ["runtime.Version","vers"], ["Modules", null] ], "identifier":"my appname", "utilization":{ "metadata_version":5, "logical_processors":16, "total_ram_mib":1024, "hostname":"my-hostname" }, "metadata":{}, "event_harvest_config": { "report_period_ms": 60000, "harvest_limits": { "analytic_event_data": 10000, "custom_event_data": %d, "log_event_data": %d, "error_event_data": 100, "span_event_data": %d } } }]`, internal.MaxLogEvents, internal.MaxCustomEvents, internal.MaxSpanEvents, internal.MaxErrorEvents, internal.MaxSpanEvents, internal.MaxTxnEvents, internal.MaxCustomEvents, internal.MaxTxnEvents, internal.MaxSpanEvents)) metadata := map[string]string{} js, err := configConnectJSONInternal(cp, 123, &utilization.SampleData, sampleEnvironment, "0.2.2", nil, metadata) if nil != err { t.Fatal(err) } out := standardizeNumbers(string(js)) if out != expect { t.Error(expect) t.Error(out) } } func TestValidate(t *testing.T) { c := Config{ License: "0123456789012345678901234567890123456789", AppName: "my app", Enabled: true, } if err := c.validate(); err != nil { t.Error(err) } c = Config{ License: "", AppName: "my app", Enabled: true, } if err := c.validate(); err != errLicenseLen { t.Error(err) } c = Config{ License: "", AppName: "my app", Enabled: false, } if err := c.validate(); err != nil { t.Error(err) } c = Config{ License: "wronglength", AppName: "my app", Enabled: true, } if err := c.validate(); err != errLicenseLen { t.Error(err) } c = Config{ License: "0123456789012345678901234567890123456789", AppName: "too;many;app;names", Enabled: true, } if err := c.validate(); err != errAppNameLimit { t.Error(err) } c = Config{ License: "0123456789012345678901234567890123456789", AppName: "", Enabled: true, } if err := c.validate(); err != errAppNameMissing { t.Error(err) } c = Config{ License: "0123456789012345678901234567890123456789", AppName: "", Enabled: false, } if err := c.validate(); err != nil { t.Error(err) } c = Config{ License: "0123456789012345678901234567890123456789", AppName: "my app", Enabled: true, HighSecurity: true, } if err := c.validate(); err != nil { t.Error(err) } } func TestValidateCalled(t *testing.T) { // Test that config validation is actually done when creating an // application. app, err := NewApplication(func(cfg *Config) { cfg.License = "" cfg.AppName = "my app" cfg.Enabled = true }) if app != nil { t.Error(app) } if err != errLicenseLen { t.Error(err) } } func TestValidateWithPoliciesToken(t *testing.T) { c := Config{ License: "0123456789012345678901234567890123456789", AppName: "my app", Enabled: true, HighSecurity: true, SecurityPoliciesToken: "0123456789", } if err := c.validate(); err != errHighSecurityWithSecurityPolicies { t.Error(err) } c = Config{ License: "0123456789012345678901234567890123456789", AppName: "my app", Enabled: true, SecurityPoliciesToken: "0123456789", } if err := c.validate(); err != nil { t.Error(err) } } func TestGatherMetadata(t *testing.T) { metadata := gatherMetadata(nil) if !reflect.DeepEqual(metadata, map[string]string{}) { t.Error(metadata) } metadata = gatherMetadata([]string{ "NEW_RELIC_METADATA_ZIP=zap", "NEW_RELIC_METADATA_PIZZA=cheese", "NEW_RELIC_METADATA_=hello", "NEW_RELIC_METADATA_LOTS_OF_EQUALS=one=two", "NEW_RELIC_METADATA_", "NEW_RELIC_METADATA_NO_EQUALS", "NEW_RELIC_METADATA_EMPTY=", "NEW_RELIC_", "hello=world", }) if !reflect.DeepEqual(metadata, map[string]string{ "NEW_RELIC_METADATA_ZIP": "zap", "NEW_RELIC_METADATA_PIZZA": "cheese", "NEW_RELIC_METADATA_": "hello", "NEW_RELIC_METADATA_LOTS_OF_EQUALS": "one=two", "NEW_RELIC_METADATA_EMPTY": "", }) { t.Error(metadata) } } func TestValidateServerless(t *testing.T) { // AppName and License can be empty in serverless mode. c := defaultConfig() c.ServerlessMode.Enabled = true if err := c.validate(); nil != err { t.Error(err) } } func TestPreconnectHost(t *testing.T) { testcases := []struct { license string override string expect string }{ { // non-region license license: "0123456789012345678901234567890123456789", override: "", expect: preconnectHostDefault, }, { // override present license: "0123456789012345678901234567890123456789", override: "other-collector.newrelic.com", expect: "other-collector.newrelic.com", }, { // four letter region license: "eu01xx6789012345678901234567890123456789", override: "", expect: "collector.eu01.nr-data.net", }, { // five letter region license: "gov01x6789012345678901234567890123456789", override: "", expect: "collector.gov01.nr-data.net", }, { // six letter region license: "foo001x6789012345678901234567890123456789", override: "", expect: "collector.foo001.nr-data.net", }, } for idx, tc := range testcases { cfg := config{Config: Config{ License: tc.license, Host: tc.override, }} if got := cfg.preconnectHost(); got != tc.expect { t.Error("testcase", idx, got, tc.expect) } } } func TestPreconnectHostCrossAgent(t *testing.T) { var testcases []struct { Name string `json:"name"` ConfigFileKey string `json:"config_file_key"` EnvKey string `json:"env_key"` ConfigOverrideHost string `json:"config_override_host"` EnvOverrideHost string `json:"env_override_host"` ExpectHostname string `json:"hostname"` } err := crossagent.ReadJSON("collector_hostname.json", &testcases) if err != nil { t.Fatal(err) } for _, tc := range testcases { // mimic file/environment precedence of other agents configKey := tc.ConfigFileKey if tc.EnvKey != "" { configKey = tc.EnvKey } overrideHost := tc.ConfigOverrideHost if tc.EnvOverrideHost != "" { overrideHost = tc.EnvOverrideHost } cfg := config{Config: Config{ License: configKey, Host: overrideHost, }} if host := cfg.preconnectHost(); host != tc.ExpectHostname { t.Errorf(`test="%s" got="%s" expected="%s"`, tc.Name, host, tc.ExpectHostname) } } } func TestComputeDynoHostname(t *testing.T) { testcases := []struct { useDynoNames bool dynoNamePrefixes []string envVarValue string expected string }{ { useDynoNames: false, envVarValue: "dynoname", expected: "", }, { useDynoNames: true, envVarValue: "", expected: "", }, { useDynoNames: true, envVarValue: "dynoname", expected: "dynoname", }, { useDynoNames: true, dynoNamePrefixes: []string{"example"}, envVarValue: "dynoname", expected: "dynoname", }, { useDynoNames: true, dynoNamePrefixes: []string{""}, envVarValue: "dynoname", expected: "dynoname", }, { useDynoNames: true, dynoNamePrefixes: []string{"example", "ex"}, envVarValue: "example.asdfasdfasdf", expected: "example.*", }, { useDynoNames: true, dynoNamePrefixes: []string{"example", "ex"}, envVarValue: "exampleasdfasdfasdf", expected: "exampleasdfasdfasdf", }, } for _, test := range testcases { getenv := func(string) string { return test.envVarValue } cfg := Config{} cfg.Heroku.UseDynoNames = test.useDynoNames cfg.Heroku.DynoNamePrefixesToShorten = test.dynoNamePrefixes if actual := cfg.computeDynoHostname(getenv); actual != test.expected { t.Errorf("unexpected output: actual=%s expected=%s", actual, test.expected) } } } func TestNewInternalConfig(t *testing.T) { labels := map[string]string{"zip": "zap"} cfg := defaultConfig() cfg.License = "0123456789012345678901234567890123456789" cfg.AppName = "my app" cfg.Logger = nil cfg.Labels = labels c, err := newInternalConfig(cfg, func(s string) string { switch s { case "DYNO": return "mydyno" } return "" }, []string{"NEW_RELIC_METADATA_ZIP=ZAP"}) if err != nil { t.Error(err) } if c.Logger == nil { t.Error("non nil Logger expected") } labels["zip"] = "1234" if c.Labels["zip"] != "zap" { t.Error("labels should have been copied", c.Labels) } if c.hostname != "mydyno" { t.Error(c.hostname) } if !reflect.DeepEqual(c.metadata, map[string]string{ "NEW_RELIC_METADATA_ZIP": "ZAP", }) { t.Error(c.metadata) } } func TestCLMScopeLabels(t *testing.T) { for i, tc := range []struct { L []string LL string V CodeLevelMetricsScope OK bool }{ {V: AllCLM, OK: true}, {L: []string{"all"}, LL: "all", V: AllCLM, OK: true}, {L: []string{"transactions"}, LL: "transactions", V: TransactionCLM, OK: true}, {L: []string{"transaction"}, LL: "transaction", V: TransactionCLM, OK: true}, {L: []string{"txn"}, LL: "txn", V: TransactionCLM, OK: true}, {L: []string{"all", "txn"}, LL: "all,txn", V: AllCLM, OK: true}, {L: []string{"undefined"}, LL: "undefined", OK: false}, } { s, ok := CodeLevelMetricsScopeLabelToValue(tc.L...) if ok != tc.OK { t.Errorf("#%d for \"%v\" expected ok=%v", i, tc.L, tc.OK) } if s != tc.V { t.Errorf("#%d for \"%v\" expected output %v, but got %v", i, tc.L, tc.V, s) } ss, ok := CodeLevelMetricsScopeLabelListToValue(tc.LL) if ok != tc.OK { t.Errorf("#%d for \"%v\" expected ok=%v", i, tc.L, tc.OK) } if ss != tc.V { t.Errorf("#%d for \"%v\" expected output %v, but got %v", i, tc.L, tc.V, ss) } } } func TestCLMJsonMarshalling(t *testing.T) { var s CodeLevelMetricsScope for i, tc := range []struct { S CodeLevelMetricsScope J string E bool }{ {S: AllCLM, J: `"all"`}, {S: TransactionCLM, J: `"transaction"`}, {S: 0x500, E: true}, } { s = tc.S j, err := json.Marshal(s) if err != nil { if !tc.E { t.Errorf("#%d generated unexpected error %v", i, err) } } else { if tc.E { t.Errorf("#%d was supposed to generate an error but didn't", i) } if tc.J != string(j) { t.Errorf("#%d expected \"%v\" but got \"%v\"", i, tc.J, string(j)) } } } } func TestCLMJsonUnmarshalling(t *testing.T) { var s CodeLevelMetricsScope for i, tc := range []struct { S CodeLevelMetricsScope J string E bool }{ {S: AllCLM, J: `"all"`}, {S: TransactionCLM, J: `"transaction"`}, {S: TransactionCLM, J: `"transaction,"`}, {S: TransactionCLM, J: `"transaction,txn"`}, {S: AllCLM, J: `"transaction,all,txn"`}, {S: AllCLM, J: `""`}, {S: AllCLM, J: `null`}, {S: AllCLM, J: `"blorfl"`, E: true}, } { err := json.Unmarshal([]byte(tc.J), &s) if err != nil { if !tc.E { t.Errorf("#%d generated unexpected error %v", i, err) } } else { if tc.E { t.Errorf("#%d was supposed to generate an error but didn't", i) } if tc.S != s { t.Errorf("#%d expected \"%v\" but got \"%v\"", i, tc.S, s) } } } } func Test_maxTxnEvents(t *testing.T) { tests := []struct { name string configured int want int }{ { name: "configured is less than 0", configured: -1, want: internal.MaxTxnEvents, }, { name: "configured is greater than max", configured: internal.MaxTxnEvents + 1, want: internal.MaxTxnEvents, }, { name: "configured is equal to max", configured: internal.MaxTxnEvents, want: internal.MaxTxnEvents, }, { name: "configured is equal to 0", configured: 0, want: 0, }, { name: "configured is between 0 and max", configured: internal.MaxTxnEvents / 2, want: internal.MaxTxnEvents / 2, }, { name: "configured is between 0 and max (small value)", configured: 100, want: 100, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxTxnEvents(tt.configured) if got != tt.want { t.Errorf("maxTxnEvents(%d) = %v, want %v", tt.configured, got, tt.want) } }) } } func Test_maxCustomEvents(t *testing.T) { tests := []struct { name string configured int want int }{ { name: "configured is less than 0", configured: -1, want: internal.MaxCustomEvents, }, { name: "configured is greater than max", configured: internal.MaxCustomEvents + 1, want: internal.MaxCustomEvents, }, { name: "configured is equal to max", configured: internal.MaxCustomEvents, want: internal.MaxCustomEvents, }, { name: "configured is equal to 0", configured: 0, want: 0, }, { name: "configured is between 0 and max", configured: internal.MaxCustomEvents / 2, want: internal.MaxCustomEvents / 2, }, { name: "configured is between 0 and max (small value)", configured: 100, want: 100, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxCustomEvents(tt.configured) if got != tt.want { t.Errorf("maxCustomEvents(%d) = %v, want %v", tt.configured, got, tt.want) } }) } } func Test_maxSpanEvents(t *testing.T) { tests := []struct { name string configured int want int }{ { name: "configured is less than 0", configured: -1, want: internal.MaxSpanEvents, }, { name: "configured is greater than max", configured: internal.MaxSpanEvents + 1, want: internal.MaxSpanEvents, }, { name: "configured is equal to max", configured: internal.MaxSpanEvents, want: internal.MaxSpanEvents, }, { name: "configured is equal to 0", configured: 0, want: 0, }, { name: "configured is between 0 and max", configured: internal.MaxSpanEvents / 2, want: internal.MaxSpanEvents / 2, }, { name: "configured is between 0 and max (small value)", configured: 100, want: 100, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxSpanEvents(tt.configured) if got != tt.want { t.Errorf("maxSpanEvents(%d) = %v, want %v", tt.configured, got, tt.want) } }) } } func Test_maxErrorEvents(t *testing.T) { tests := []struct { name string configured int want int }{ { name: "configured is less than 0", configured: -1, want: internal.MaxErrorEvents, }, { name: "configured is greater than max", configured: internal.MaxErrorEvents + 1, want: internal.MaxErrorEvents, }, { name: "configured is equal to max", configured: internal.MaxErrorEvents, want: internal.MaxErrorEvents, }, { name: "configured is equal to 0", configured: 0, want: 0, }, { name: "configured is between 0 and max", configured: internal.MaxErrorEvents / 2, want: internal.MaxErrorEvents / 2, }, { name: "configured is between 0 and max (small value)", configured: 10, want: 10, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxErrorEvents(tt.configured) if got != tt.want { t.Errorf("maxErrorEvents(%d) = %v, want %v", tt.configured, got, tt.want) } }) } } func Test_maxLogEvents(t *testing.T) { tests := []struct { name string configured int want int }{ { name: "configured is less than 0", configured: -1, want: internal.MaxLogEvents, }, { name: "configured is greater than max", configured: internal.MaxLogEvents + 1, want: internal.MaxLogEvents, }, { name: "configured is equal to max", configured: internal.MaxLogEvents, want: internal.MaxLogEvents, }, { name: "configured is equal to 0", configured: 0, want: 0, }, { name: "configured is between 0 and max", configured: internal.MaxLogEvents / 2, want: internal.MaxLogEvents / 2, }, { name: "configured is between 0 and max (small value)", configured: 100, want: 100, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxLogEvents(tt.configured) if got != tt.want { t.Errorf("maxLogEvents(%d) = %v, want %v", tt.configured, got, tt.want) } }) } } func TestDefaultConfigMaxSamplesStored(t *testing.T) { cfg := defaultConfig() tests := []struct { name string actual int expected int }{ { name: "CustomInsightsEvents.MaxSamplesStored", actual: cfg.CustomInsightsEvents.MaxSamplesStored, expected: internal.MaxCustomEvents, }, { name: "TransactionEvents.MaxSamplesStored", actual: cfg.TransactionEvents.MaxSamplesStored, expected: internal.MaxTxnEvents, }, { name: "ErrorCollector.MaxSamplesStored", actual: cfg.ErrorCollector.MaxSamplesStored, expected: internal.MaxErrorEvents, }, { name: "SpanEvents.MaxSamplesStored", actual: cfg.SpanEvents.MaxSamplesStored, expected: internal.MaxSpanEvents, }, { name: "ApplicationLogging.Forwarding.MaxSamplesStored", actual: cfg.ApplicationLogging.Forwarding.MaxSamplesStored, expected: internal.MaxLogEvents, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.actual != tt.expected { t.Errorf("%s: defaultConfig() sets %d, expected %d", tt.name, tt.actual, tt.expected) } }) } } go-agent-3.42.0/v3/newrelic/context.go000066400000000000000000000031511510742411500174540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "context" "net/http" "github.com/newrelic/go-agent/v3/internal" ) // NewContext returns a new context.Context that carries the provided // transaction. func NewContext(ctx context.Context, txn *Transaction) context.Context { return context.WithValue(ctx, internal.TransactionContextKey, txn) } // FromContext returns the Transaction from the context if present, and nil // otherwise. func FromContext(ctx context.Context) *Transaction { if nil == ctx { return nil } h, _ := ctx.Value(internal.TransactionContextKey).(*Transaction) if nil != h { return h } // If we couldn't find a transaction using // internal.TransactionContextKey, try with // internal.GinTransactionContextKey. Unfortunately, gin.Context.Set // requires a string key, so we cannot use // internal.TransactionContextKey in nrgin.Middleware. We check for two // keys (rather than turning internal.TransactionContextKey into a // string key) because context.WithValue will cause golint to complain // if used with a string key. h, _ = ctx.Value(internal.GinTransactionContextKey).(*Transaction) return h } // RequestWithTransactionContext adds the Transaction to the request's context. func RequestWithTransactionContext(req *http.Request, txn *Transaction) *http.Request { ctx := req.Context() ctx = NewContext(ctx, txn) return req.WithContext(ctx) } func transactionFromRequestContext(req *http.Request) *Transaction { var txn *Transaction if nil != req { txn = FromContext(req.Context()) } return txn } go-agent-3.42.0/v3/newrelic/cross_process_http.go000066400000000000000000000037571510742411500217320ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "github.com/newrelic/go-agent/v3/internal/cat" ) // InboundHTTPRequest adds the inbound request metadata to the txnCrossProcess. func (txp *txnCrossProcess) InboundHTTPRequest(hdr http.Header) error { return txp.handleInboundRequestHeaders(httpHeaderToMetadata(hdr)) } // appDataToHTTPHeader encapsulates the given appData value in the correct HTTP // header. func appDataToHTTPHeader(appData string) http.Header { header := http.Header{} if appData != "" { header.Add(cat.NewRelicAppDataName, appData) } return header } // httpHeaderToAppData gets the appData value from the correct HTTP header. func httpHeaderToAppData(header http.Header) string { if header == nil { return "" } return header.Get(cat.NewRelicAppDataName) } // httpHeaderToMetadata gets the cross process metadata from the relevant HTTP // headers. func httpHeaderToMetadata(header http.Header) crossProcessMetadata { if header == nil { return crossProcessMetadata{} } return crossProcessMetadata{ ID: header.Get(cat.NewRelicIDName), TxnData: header.Get(cat.NewRelicTxnName), Synthetics: header.Get(cat.NewRelicSyntheticsName), SyntheticsInfo: header.Get(cat.NewRelicSyntheticsInfo), } } // metadataToHTTPHeader creates a set of HTTP headers to represent the given // cross process metadata. func metadataToHTTPHeader(metadata crossProcessMetadata) http.Header { header := http.Header{} if metadata.ID != "" { header.Add(cat.NewRelicIDName, metadata.ID) } if metadata.TxnData != "" { header.Add(cat.NewRelicTxnName, metadata.TxnData) } if metadata.Synthetics != "" { header.Add(cat.NewRelicSyntheticsName, metadata.Synthetics) // This header will only be present when the `X-NewRelic-Synthetics` header is present if metadata.SyntheticsInfo != "" { header.Add(cat.NewRelicSyntheticsInfo, metadata.SyntheticsInfo) } } return header } go-agent-3.42.0/v3/newrelic/cross_process_http_test.go000066400000000000000000000131321510742411500227550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "reflect" "testing" "github.com/newrelic/go-agent/v3/internal/cat" ) func TestTxnCrossProcessInitFromHTTPRequest(t *testing.T) { txp := &txnCrossProcess{} txp.Init(true, false, replyAccountOne) if txp.IsInbound() { t.Error("inbound CAT enabled even though there was no request") } txp = &txnCrossProcess{} req, err := http.NewRequest("GET", "http://foo.bar/", nil) if err != nil { t.Fatal(err) } txp.Init(true, false, replyAccountOne) if err := txp.InboundHTTPRequest(req.Header); err != nil { t.Errorf("got error while consuming an empty request: %v", err) } if txp.IsInbound() { t.Error("inbound CAT enabled even though there was no metadata in the request") } txp = &txnCrossProcess{} req, err = http.NewRequest("GET", "http://foo.bar/", nil) if err != nil { t.Fatal(err) } req.Header.Add(cat.NewRelicIDName, mustObfuscate(`1#1`, "foo")) req.Header.Add(cat.NewRelicTxnName, mustObfuscate(`["abcdefgh",false,"12345678","b95be233"]`, "foo")) txp.Init(true, false, replyAccountOne) if err := txp.InboundHTTPRequest(req.Header); err != nil { t.Errorf("got error while consuming an inbound CAT request: %v", err) } // A second call to InboundHTTPRequest to ensure that it can safely // be called multiple times: if err := txp.InboundHTTPRequest(req.Header); err != nil { t.Errorf("got error while consuming an inbound CAT request: %v", err) } if !txp.IsInbound() { t.Error("inbound CAT disabled even though there was metadata in the request") } if txp.ClientID != "1#1" { t.Errorf("incorrect ClientID: %s", txp.ClientID) } if txp.ReferringTxnGUID != "abcdefgh" { t.Errorf("incorrect ReferringTxnGUID: %s", txp.ReferringTxnGUID) } if txp.TripID != "12345678" { t.Errorf("incorrect TripID: %s", txp.TripID) } if txp.ReferringPathHash != "b95be233" { t.Errorf("incorrect ReferringPathHash: %s", txp.ReferringPathHash) } } func TestAppDataToHTTPHeader(t *testing.T) { header := appDataToHTTPHeader("") if len(header) != 0 { t.Errorf("unexpected number of header elements: %d", len(header)) } header = appDataToHTTPHeader("foo") if len(header) != 1 { t.Errorf("unexpected number of header elements: %d", len(header)) } if actual := header.Get(cat.NewRelicAppDataName); actual != "foo" { t.Errorf("unexpected header value: %s", actual) } } func TestHTTPHeaderToAppData(t *testing.T) { if appData := httpHeaderToAppData(nil); appData != "" { t.Errorf("unexpected app data: %s", appData) } header := http.Header{} if appData := httpHeaderToAppData(header); appData != "" { t.Errorf("unexpected app data: %s", appData) } header.Add("X-Foo", "bar") if appData := httpHeaderToAppData(header); appData != "" { t.Errorf("unexpected app data: %s", appData) } header.Add(cat.NewRelicAppDataName, "foo") if appData := httpHeaderToAppData(header); appData != "foo" { t.Errorf("unexpected app data: %s", appData) } } func TestHTTPHeaderToMetadata(t *testing.T) { if metadata := httpHeaderToMetadata(nil); !reflect.DeepEqual(metadata, crossProcessMetadata{}) { t.Errorf("unexpected metadata: %v", metadata) } header := http.Header{} if metadata := httpHeaderToMetadata(header); !reflect.DeepEqual(metadata, crossProcessMetadata{}) { t.Errorf("unexpected metadata: %v", metadata) } header.Add("X-Foo", "bar") if metadata := httpHeaderToMetadata(header); !reflect.DeepEqual(metadata, crossProcessMetadata{}) { t.Errorf("unexpected metadata: %v", metadata) } header.Add(cat.NewRelicIDName, "id") if metadata := httpHeaderToMetadata(header); !reflect.DeepEqual(metadata, crossProcessMetadata{ ID: "id", }) { t.Errorf("unexpected metadata: %v", metadata) } header.Add(cat.NewRelicTxnName, "txn") if metadata := httpHeaderToMetadata(header); !reflect.DeepEqual(metadata, crossProcessMetadata{ ID: "id", TxnData: "txn", }) { t.Errorf("unexpected metadata: %v", metadata) } header.Add(cat.NewRelicSyntheticsName, "synth") if metadata := httpHeaderToMetadata(header); !reflect.DeepEqual(metadata, crossProcessMetadata{ ID: "id", TxnData: "txn", Synthetics: "synth", }) { t.Errorf("unexpected metadata: %v", metadata) } } func TestMetadataToHTTPHeader(t *testing.T) { metadata := crossProcessMetadata{} header := metadataToHTTPHeader(metadata) if len(header) != 0 { t.Errorf("unexpected number of header elements: %d", len(header)) } metadata.ID = "id" header = metadataToHTTPHeader(metadata) if len(header) != 1 { t.Errorf("unexpected number of header elements: %d", len(header)) } if actual := header.Get(cat.NewRelicIDName); actual != "id" { t.Errorf("unexpected header value: %s", actual) } metadata.TxnData = "txn" header = metadataToHTTPHeader(metadata) if len(header) != 2 { t.Errorf("unexpected number of header elements: %d", len(header)) } if actual := header.Get(cat.NewRelicIDName); actual != "id" { t.Errorf("unexpected header value: %s", actual) } if actual := header.Get(cat.NewRelicTxnName); actual != "txn" { t.Errorf("unexpected header value: %s", actual) } metadata.Synthetics = "synth" header = metadataToHTTPHeader(metadata) if len(header) != 3 { t.Errorf("unexpected number of header elements: %d", len(header)) } if actual := header.Get(cat.NewRelicIDName); actual != "id" { t.Errorf("unexpected header value: %s", actual) } if actual := header.Get(cat.NewRelicTxnName); actual != "txn" { t.Errorf("unexpected header value: %s", actual) } if actual := header.Get(cat.NewRelicSyntheticsName); actual != "synth" { t.Errorf("unexpected header value: %s", actual) } } go-agent-3.42.0/v3/newrelic/custom_event.go000066400000000000000000000064241510742411500205110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "fmt" "regexp" "time" ) // https://newrelic.atlassian.net/wiki/display/eng/Custom+Events+in+New+Relic+Agents var ( eventTypeRegexRaw = `^[a-zA-Z0-9:_ ]+$` eventTypeRegex = regexp.MustCompile(eventTypeRegexRaw) errEventTypeLength = fmt.Errorf("event type exceeds length limit of %d", attributeKeyLengthLimit) // errEventTypeRegex will be returned to caller of app.RecordCustomEvent // if the event type is not valid. errEventTypeRegex = fmt.Errorf("event type must match %s", eventTypeRegexRaw) errNumAttributes = fmt.Errorf("maximum of %d attributes exceeded", customEventAttributeLimit) ) // customEvent is a custom event. type customEvent struct { eventType string timestamp time.Time truncatedParams map[string]interface{} } // WriteJSON prepares JSON in the format expected by the collector. func (e *customEvent) WriteJSON(buf *bytes.Buffer) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('[') buf.WriteByte('{') w.stringField("type", e.eventType) w.intField("timestamp", timeToIntMillis(e.timestamp)) buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('{') w = jsonFieldsWriter{buf: buf} for key, val := range e.truncatedParams { writeAttributeValueJSON(&w, key, val) } buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('{') buf.WriteByte('}') buf.WriteByte(']') } // MarshalJSON is used for testing. func (e *customEvent) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, 256)) e.WriteJSON(buf) return buf.Bytes(), nil } func eventTypeValidate(eventType string) error { if len(eventType) > attributeKeyLengthLimit { return errEventTypeLength } if !eventTypeRegex.MatchString(eventType) { return errEventTypeRegex } return nil } // CreateCustomEvent creates a custom event. func createCustomEvent(eventType string, params map[string]interface{}, now time.Time) (*customEvent, error) { if err := eventTypeValidate(eventType); nil != err { return nil, err } if len(params) > customEventAttributeLimit { return nil, errNumAttributes } truncatedParams := make(map[string]interface{}) for key, val := range params { val, err := validateUserAttribute(key, val) if nil != err { return nil, err } truncatedParams[key] = val } return &customEvent{ eventType: eventType, timestamp: now, truncatedParams: truncatedParams, }, nil } // CreateCustomEventUnlimitedSize creates a custom event without restricting string value length. func createCustomEventUnlimitedSize(eventType string, params map[string]interface{}, now time.Time) (*customEvent, error) { if err := eventTypeValidate(eventType); err != nil { return nil, err } if len(params) > customEventAttributeLimit { return nil, errNumAttributes } truncatedParams := make(map[string]interface{}) for key, val := range params { val, err := validateUserAttributeUnlimitedSize(key, val) if err != nil { return nil, err } truncatedParams[key] = val } return &customEvent{ eventType: eventType, timestamp: now, truncatedParams: truncatedParams, }, nil } // MergeIntoHarvest implements Harvestable. func (e *customEvent) MergeIntoHarvest(h *harvest) { h.CustomEvents.Add(e) } go-agent-3.42.0/v3/newrelic/custom_event_test.go000066400000000000000000000131431510742411500215440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "strconv" "testing" "time" ) var ( now = time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) strLen512 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" strLen255 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ) // Tests use a single key-value pair in params to ensure deterministic JSON // ordering. func TestCreateCustomEventSuccess(t *testing.T) { event, err := createCustomEvent("myEvent", map[string]interface{}{"alpha": 1}, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{"alpha":1},{}]` { t.Fatal(string(js)) } } func TestInvalidEventTypeCharacter(t *testing.T) { event, err := createCustomEvent("myEvent!", map[string]interface{}{"alpha": 1}, now) if err != errEventTypeRegex { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestLongEventType(t *testing.T) { event, err := createCustomEvent(strLen512, map[string]interface{}{"alpha": 1}, now) if err != errEventTypeLength { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestNilParams(t *testing.T) { event, err := createCustomEvent("myEvent", nil, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{},{}]` { t.Fatal(string(js)) } } func TestMissingEventType(t *testing.T) { event, err := createCustomEvent("", map[string]interface{}{"alpha": 1}, now) if err != errEventTypeRegex { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestEmptyParams(t *testing.T) { event, err := createCustomEvent("myEvent", map[string]interface{}{}, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{},{}]` { t.Fatal(string(js)) } } func TestTruncatedStringValue(t *testing.T) { event, err := createCustomEvent("myEvent", map[string]interface{}{"alpha": strLen512}, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{"alpha":"`+strLen255+`"},{}]` { t.Fatal(string(js)) } } func TestInvalidValueType(t *testing.T) { event, err := createCustomEvent("myEvent", map[string]interface{}{"alpha": []string{}}, now) if _, ok := err.(errInvalidAttributeType); !ok { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestInvalidCustomAttributeKey(t *testing.T) { event, err := createCustomEvent("myEvent", map[string]interface{}{strLen512: 1}, now) if nil == err { t.Fatal(err) } if _, ok := err.(invalidAttributeKeyErr); !ok { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestTooManyAttributes(t *testing.T) { params := make(map[string]interface{}) for i := 0; i < customEventAttributeLimit+1; i++ { params[strconv.Itoa(i)] = i } event, err := createCustomEvent("myEvent", params, now) if errNumAttributes != err { t.Fatal(err) } if nil != event { t.Fatal(event) } } func TestCustomEventAttributeTypes(t *testing.T) { testcases := []struct { val interface{} js string }{ {"string", `"string"`}, {true, `true`}, {false, `false`}, {uint8(1), `1`}, {uint16(1), `1`}, {uint32(1), `1`}, {uint64(1), `1`}, {int8(1), `1`}, {int16(1), `1`}, {int32(1), `1`}, {int64(1), `1`}, {float32(1), `1`}, {float64(1), `1`}, {uint(1), `1`}, {int(1), `1`}, {uintptr(1), `1`}, } for _, tc := range testcases { event, err := createCustomEvent("myEvent", map[string]interface{}{"key": tc.val}, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{"key":`+tc.js+`},{}]` { t.Fatal(string(js)) } } } func TestCustomParamsCopied(t *testing.T) { params := map[string]interface{}{"alpha": 1} event, err := createCustomEvent("myEvent", params, now) if nil != err { t.Fatal(err) } // Attempt to change the params after the event created: params["zip"] = "zap" js, err := json.Marshal(event) if nil != err { t.Fatal(err) } if string(js) != `[{"type":"myEvent","timestamp":1417136460000},{"alpha":1},{}]` { t.Fatal(string(js)) } } func TestMultipleAttributeJSON(t *testing.T) { params := map[string]interface{}{"alpha": 1, "beta": 2} event, err := createCustomEvent("myEvent", params, now) if nil != err { t.Fatal(err) } js, err := json.Marshal(event) if nil != err { t.Fatal(err) } // Params order may not be deterministic, so we simply test that the // JSON created is valid. var valid interface{} if err := json.Unmarshal(js, &valid); nil != err { t.Error(string(js)) } } go-agent-3.42.0/v3/newrelic/custom_events.go000066400000000000000000000017061510742411500206720ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "time" type customEvents struct { *analyticsEvents } func newCustomEvents(max int) *customEvents { return &customEvents{ analyticsEvents: newAnalyticsEvents(max), } } func (cs *customEvents) Add(e *customEvent) { // For the Go Agent, customEvents are added to the application, not the transaction. // As a result, customEvents do not inherit their priority from the transaction, though // they are still sampled according to priority sampling. priority := newPriority() cs.addEvent(analyticsEvent{priority, e}) } func (cs *customEvents) MergeIntoHarvest(h *harvest) { h.CustomEvents.mergeFailed(cs.analyticsEvents) } func (cs *customEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return cs.CollectorJSON(agentRunID) } func (cs *customEvents) EndpointMethod() string { return cmdCustomEvents } go-agent-3.42.0/v3/newrelic/custom_metric.go000066400000000000000000000006251510742411500206500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic // customMetric is a custom metric. type customMetric struct { RawInputName string Value float64 } // MergeIntoHarvest implements Harvestable. func (m customMetric) MergeIntoHarvest(h *harvest) { h.Metrics.addValue(customMetricName(m.RawInputName), "", m.Value, unforced) } go-agent-3.42.0/v3/newrelic/datastore.go000066400000000000000000000030261510742411500177570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic // DatastoreProduct is used to identify your datastore type in New Relic. It // is used in the DatastoreSegment Product field. type DatastoreProduct string // Datastore names used across New Relic agents: const ( DatastoreCassandra DatastoreProduct = "Cassandra" DatastoreCouchDB DatastoreProduct = "CouchDB" DatastoreDerby DatastoreProduct = "Derby" DatastoreDynamoDB DatastoreProduct = "DynamoDB" DatastoreElasticsearch DatastoreProduct = "Elasticsearch" DatastoreFirebird DatastoreProduct = "Firebird" DatastoreIBMDB2 DatastoreProduct = "IBMDB2" DatastoreInformix DatastoreProduct = "Informix" DatastoreMemcached DatastoreProduct = "Memcached" DatastoreMongoDB DatastoreProduct = "MongoDB" DatastoreMSSQL DatastoreProduct = "MSSQL" DatastoreMySQL DatastoreProduct = "MySQL" DatastoreNeptune DatastoreProduct = "Neptune" DatastoreOracle DatastoreProduct = "Oracle" DatastorePostgres DatastoreProduct = "Postgres" DatastoreRedis DatastoreProduct = "Redis" DatastoreRiak DatastoreProduct = "Riak" DatastoreSnowflake DatastoreProduct = "Snowflake" DatastoreSolr DatastoreProduct = "Solr" DatastoreSQLite DatastoreProduct = "SQLite" DatastoreTarantool DatastoreProduct = "Tarantool" DatastoreVoltDB DatastoreProduct = "VoltDB" DatastoreAerospike DatastoreProduct = "Aerospike" ) go-agent-3.42.0/v3/newrelic/distributed_tracing.go000066400000000000000000000336731510742411500220350ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "regexp" "strconv" "strings" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/jsonx" ) type distTraceVersion [2]int func (v distTraceVersion) major() int { return v[0] } func (v distTraceVersion) minor() int { return v[1] } // WriteJSON implements the functionality to support writerField // in our internal json builder. It appends the JSON representation // of a distTraceVersion to the destination bytes.Buffer. func (v distTraceVersion) WriteJSON(buf *bytes.Buffer) { jsonx.AppendIntArray(buf, int64(v[0]), int64(v[1])) } const ( // callerTypeApp is the Type field's value for outbound payloads. callerTypeApp = "App" // callerTypeBrowser is the Type field's value for browser payloads callerTypeBrowser = "Browser" // callerTypeMobile is the Type field's value for mobile payloads callerTypeMobile = "Mobile" ) var ( currentDistTraceVersion = distTraceVersion([2]int{0 /* Major */, 1 /* Minor */}) callerUnknown = payloadCaller{Type: "Unknown", App: "Unknown", Account: "Unknown", TransportType: "Unknown"} traceParentRegex = regexp.MustCompile(`^([a-f0-9]{2})-` + // version `([a-f0-9]{32})-` + // traceId `([a-f0-9]{16})-` + // parentId `([a-f0-9]{2})(-.*)?$`) // flags ) // timestampMillis allows raw payloads to use exact times, and marshalled // payloads to use times in millis. type timestampMillis time.Time func (tm *timestampMillis) UnmarshalJSON(data []byte) error { var millis uint64 if err := json.Unmarshal(data, &millis); nil != err { return err } *tm = timestampMillis(timeFromUnixMilliseconds(millis)) return nil } func (tm timestampMillis) MarshalJSON() ([]byte, error) { return json.Marshal(timeToUnixMilliseconds(tm.Time())) } // WriteJSON implements the functionality to support writerField // in our internal json builder. It appends the JSON representation // of a timestampMillis value to the destination bytes.Buffer. func (tm timestampMillis) WriteJSON(buf *bytes.Buffer) { jsonx.AppendUint(buf, timeToUnixMilliseconds(tm.Time())) } func (tm timestampMillis) Time() time.Time { return time.Time(tm) } func (tm *timestampMillis) Set(t time.Time) { *tm = timestampMillis(t) } func (tm timestampMillis) unixMillisecondsString() string { ms := timeToUnixMilliseconds(tm.Time()) return strconv.FormatUint(ms, 10) } // payload is the distributed tracing payload. type payload struct { Type string `json:"ty"` App string `json:"ap"` Account string `json:"ac"` TransactionID string `json:"tx,omitempty"` ID string `json:"id,omitempty"` TracedID string `json:"tr"` Priority priority `json:"pr"` // This is a *bool instead of a normal bool so we can tell the different between unset and false. Sampled *bool `json:"sa"` Timestamp timestampMillis `json:"ti"` TransportDuration time.Duration `json:"-"` TrustedParentID string `json:"-"` TracingVendors string `json:"-"` HasNewRelicTraceInfo bool `json:"-"` TrustedAccountKey string `json:"tk,omitempty"` NonTrustedTraceState string `json:"-"` OriginalTraceState string `json:"-"` } // WriteJSON implements the functionality to support writerField // in our internal json builder. It appends the JSON representation // of a payload struct to the destination bytes.Buffer. func (p payload) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('{') w := jsonFieldsWriter{buf: buf} w.stringField("ty", p.Type) w.stringField("ap", p.App) w.stringField("ac", p.Account) if p.TransactionID != "" { w.stringField("tx", p.TransactionID) } if p.ID != "" { w.stringField("id", p.ID) } w.stringField("tr", p.TracedID) w.float32Field("pr", float32(p.Priority)) if p.Sampled == nil { w.addKey("sa") w.buf.WriteString("null") } else { w.boolField("sa", *p.Sampled) } w.writerField("ti", p.Timestamp) if p.TrustedAccountKey != "" { w.stringField("tk", p.TrustedAccountKey) } buf.WriteByte('}') } type payloadCaller struct { TransportType string Type string App string Account string } var ( errPayloadMissingGUIDTxnID = errors.New("payload is missing both guid/id and TransactionId/tx") errPayloadMissingType = errors.New("payload is missing Type/ty") errPayloadMissingAccount = errors.New("payload is missing Account/ac") errPayloadMissingApp = errors.New("payload is missing App/ap") errPayloadMissingTraceID = errors.New("payload is missing TracedID/tr") errPayloadMissingTimestamp = errors.New("payload is missing Timestamp/ti") errPayloadMissingVersion = errors.New("payload is missing Version/v") ) // IsValid IsValidNewRelicData the payload data by looking for missing fields. // Returns an error if there's a problem, nil if everything's fine func (p payload) validateNewRelicData() error { // If a payload is missing both `guid` and `transactionId` is received, // a ParseException supportability metric should be generated. if p.TransactionID == "" && p.ID == "" { return errPayloadMissingGUIDTxnID } if p.Type == "" { return errPayloadMissingType } if p.Account == "" { return errPayloadMissingAccount } if p.App == "" { return errPayloadMissingApp } if p.TracedID == "" { return errPayloadMissingTraceID } if p.Timestamp.Time().IsZero() || p.Timestamp.Time().Unix() == 0 { return errPayloadMissingTimestamp } return nil } const payloadJSONStartingSizeEstimate = 256 func (p payload) text(v distTraceVersion) []byte { // TrustedAccountKey should only be attached to the outbound payload if its value differs // from the Account field. if p.TrustedAccountKey == p.Account { p.TrustedAccountKey = "" } js := bytes.NewBuffer(make([]byte, 0, payloadJSONStartingSizeEstimate)) w := jsonFieldsWriter{ buf: js, } js.WriteByte('{') w.writerField("v", v) w.writerField("d", p) js.WriteByte('}') return js.Bytes() } // NRText implements newrelic.DistributedTracePayload. func (p payload) NRText() string { t := p.text(currentDistTraceVersion) return string(t) } // NRHTTPSafe implements newrelic.DistributedTracePayload. func (p payload) NRHTTPSafe() string { t := p.text(currentDistTraceVersion) return base64.StdEncoding.EncodeToString(t) } var ( typeMap = map[string]string{ callerTypeApp: "0", callerTypeBrowser: "1", callerTypeMobile: "2", } typeMapReverse = func() map[string]string { reversed := make(map[string]string) for k, v := range typeMap { reversed[v] = k } return reversed }() ) const ( w3cVersion = "00" traceStateVersion = "0" ) // W3CTraceParent returns the W3C TraceParent header for this payload func (p payload) W3CTraceParent() string { var flags string if p.isSampled() { flags = "01" } else { flags = "00" } traceID := strings.ToLower(p.TracedID) if idLen := len(traceID); idLen < internal.TraceIDHexStringLen { traceID = strings.Repeat("0", internal.TraceIDHexStringLen-idLen) + traceID } else if idLen > internal.TraceIDHexStringLen { traceID = traceID[idLen-internal.TraceIDHexStringLen:] } return w3cVersion + "-" + traceID + "-" + p.ID + "-" + flags } // W3CTraceState returns the W3C TraceState header for this payload func (p payload) W3CTraceState() string { var flags string if p.isSampled() { flags = "1" } else { flags = "0" } state := p.TrustedAccountKey + "@nr=" + traceStateVersion + "-" + typeMap[p.Type] + "-" + p.Account + "-" + p.App + "-" + p.ID + "-" + p.TransactionID + "-" + flags + "-" + p.Priority.traceStateFormat() + "-" + p.Timestamp.unixMillisecondsString() if p.NonTrustedTraceState != "" { state += "," + p.NonTrustedTraceState } return state } var ( trueVal = true falseVal = false boolPtrs = map[bool]*bool{ true: &trueVal, false: &falseVal, } ) // SetSampled lets us set a value for our *bool, // which we can't do directly since a pointer // needs something to point at. func (p *payload) SetSampled(sampled bool) { p.Sampled = boolPtrs[sampled] } func (p payload) isSampled() bool { return p.Sampled != nil && *p.Sampled } // acceptPayload parses the inbound distributed tracing payload. func acceptPayload(hdrs http.Header, trustedAccountKey string, support *distributedTracingSupport) (*payload, error) { if hdrs.Get(DistributedTraceW3CTraceParentHeader) != "" { return processW3CHeaders(hdrs, trustedAccountKey, support) } return processNRDTString(hdrs.Get(DistributedTraceNewRelicHeader), support) } func processNRDTString(str string, support *distributedTracingSupport) (*payload, error) { if str == "" { return nil, nil } var decoded []byte if str[0] == '{' { decoded = []byte(str) } else { var err error decoded, err = base64.StdEncoding.DecodeString(str) if err != nil { support.AcceptPayloadParseException = true return nil, fmt.Errorf("unable to decode payload: %v", err) } } envelope := struct { Version distTraceVersion `json:"v"` Data json.RawMessage `json:"d"` }{} if err := json.Unmarshal(decoded, &envelope); err != nil { support.AcceptPayloadParseException = true return nil, fmt.Errorf("unable to unmarshal payload: %v", err) } if envelope.Version.major() == 0 && envelope.Version.minor() == 0 { support.AcceptPayloadParseException = true return nil, errPayloadMissingVersion } if envelope.Version.major() > currentDistTraceVersion.major() { support.AcceptPayloadIgnoredVersion = true return nil, fmt.Errorf("unsupported major version number %v", envelope.Version.major()) } payload := new(payload) if err := json.Unmarshal(envelope.Data, payload); err != nil { support.AcceptPayloadParseException = true return nil, fmt.Errorf("unable to unmarshal payload data: %v", err) } payload.HasNewRelicTraceInfo = true if err := payload.validateNewRelicData(); err != nil { support.AcceptPayloadParseException = true return nil, err } support.AcceptPayloadSuccess = true return payload, nil } func processW3CHeaders(hdrs http.Header, trustedAccountKey string, support *distributedTracingSupport) (*payload, error) { p, err := processTraceParent(hdrs) if err != nil { support.TraceContextParentParseException = true return nil, err } err = processTraceState(hdrs, trustedAccountKey, p) if err != nil { if err == errInvalidNRTraceState { support.TraceContextStateInvalidNrEntry = true } else { support.TraceContextStateNoNrEntry = true } } support.TraceContextAcceptSuccess = true return p, nil } var ( errTooManyHdrs = errors.New("too many TraceParent headers") errNumEntries = errors.New("invalid number of TraceParent entries") errInvalidTraceID = errors.New("invalid TraceParent trace ID") errInvalidParentID = errors.New("invalid TraceParent parent ID") errInvalidFlags = errors.New("invalid TraceParent flags for this version") errInvalidNRTraceState = errors.New("invalid NR entry in trace state") errMissingTrustedNR = errors.New("no trusted NR entry found in trace state") ) func processTraceParent(hdrs http.Header) (*payload, error) { traceParents := hdrs[DistributedTraceW3CTraceParentHeader] if len(traceParents) > 1 { return nil, errTooManyHdrs } subMatches := traceParentRegex.FindStringSubmatch(traceParents[0]) if subMatches == nil || len(subMatches) != 6 { return nil, errNumEntries } if !validateVersionAndFlags(subMatches) { return nil, errInvalidFlags } p := new(payload) p.TracedID = subMatches[2] if p.TracedID == "00000000000000000000000000000000" { return nil, errInvalidTraceID } p.ID = subMatches[3] if p.ID == "0000000000000000" { return nil, errInvalidParentID } return p, nil } func validateVersionAndFlags(subMatches []string) bool { if subMatches[1] == w3cVersion { if subMatches[5] != "" { return false } } // Invalid version: https://w3c.github.io/trace-context/#version if subMatches[1] == "ff" { return false } return true } func processTraceState(hdrs http.Header, trustedAccountKey string, p *payload) error { traceStates := hdrs[DistributedTraceW3CTraceStateHeader] fullTraceState := strings.Join(traceStates, ",") p.OriginalTraceState = fullTraceState var trustedVal string p.TracingVendors, p.NonTrustedTraceState, trustedVal = parseTraceState(fullTraceState, trustedAccountKey) if trustedVal == "" { return errMissingTrustedNR } matches := strings.Split(trustedVal, "-") if len(matches) < 9 { return errInvalidNRTraceState } // Required Fields: version := matches[0] parentType := typeMapReverse[matches[1]] account := matches[2] app := matches[3] timestamp, err := strconv.ParseUint(matches[8], 10, 64) if err != nil || version == "" || parentType == "" || account == "" || app == "" { return errInvalidNRTraceState } p.TrustedAccountKey = trustedAccountKey p.Type = parentType p.Account = account p.App = app p.TrustedParentID = matches[4] p.TransactionID = matches[5] // If sampled isn't "1" or "0", leave it unset if matches[6] == "1" { p.SetSampled(true) } else if matches[6] == "0" { p.SetSampled(false) } pty, err := strconv.ParseFloat(matches[7], 32) if nil == err { p.Priority = priority(pty) } p.Timestamp = timestampMillis(timeFromUnixMilliseconds(timestamp)) p.HasNewRelicTraceInfo = true return nil } func parseTraceState(fullState, trustedAccountKey string) (nonTrustedVendors string, nonTrustedState string, trustedEntryValue string) { trustedKey := trustedAccountKey + "@nr" pairs := strings.Split(fullState, ",") vendors := make([]string, 0, len(pairs)) states := make([]string, 0, len(pairs)) for _, entry := range pairs { entry = strings.TrimSpace(entry) m := strings.Split(entry, "=") if len(m) != 2 { continue } if key, val := m[0], m[1]; key == trustedKey { trustedEntryValue = val } else { vendors = append(vendors, key) states = append(states, entry) } } nonTrustedVendors = strings.Join(vendors, ",") nonTrustedState = strings.Join(states, ",") return } go-agent-3.42.0/v3/newrelic/distributed_tracing_test.go000066400000000000000000000544631510742411500230740ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "net/http" "reflect" "testing" "time" ) var ( samplePayload = payload{ Type: callerTypeApp, Account: "123", App: "456", ID: "myid", TracedID: "mytrip", Priority: 0.12345, Timestamp: timestampMillis(time.Now()), HasNewRelicTraceInfo: true, } ) func TestPayloadNil(t *testing.T) { var support distributedTracingSupport out, err := acceptPayload(nil, "123", &support) if err != nil || out != nil { t.Fatal(err, out) } if !support.isEmpty() { t.Error("support flags expected to be empty", support) } } func TestPayloadText(t *testing.T) { hdrs := http.Header{} hdrs.Set(DistributedTraceNewRelicHeader, samplePayload.NRText()) var support distributedTracingSupport out, err := acceptPayload(hdrs, "123", &support) if err != nil || out == nil { t.Fatal(err, out) } if !support.AcceptPayloadSuccess { t.Error("unexpected support flags", support) } out.Timestamp = samplePayload.Timestamp // account for timezone differences if samplePayload != *out { t.Fatal(samplePayload, out) } } func TestPayloadHTTPSafe(t *testing.T) { hdrs := http.Header{} hdrs.Set(DistributedTraceNewRelicHeader, samplePayload.NRHTTPSafe()) var support distributedTracingSupport out, err := acceptPayload(hdrs, "123", &support) if err != nil || nil == out { t.Fatal(err, out) } if !support.AcceptPayloadSuccess { t.Error("unexpected support flags", support) } out.Timestamp = samplePayload.Timestamp // account for timezone differences if samplePayload != *out { t.Fatal(samplePayload, out) } } func TestTimestampMillisMarshalUnmarshal(t *testing.T) { var sec int64 = 111 var millis int64 = 222 var micros int64 = 333 var nsecWithMicros = 1000*1000*millis + 1000*micros var nsecWithoutMicros = 1000 * 1000 * millis input := time.Unix(sec, nsecWithMicros) expectOutput := time.Unix(sec, nsecWithoutMicros) var tm timestampMillis tm.Set(input) js, err := json.Marshal(tm) if nil != err { t.Fatal(err) } var out timestampMillis err = json.Unmarshal(js, &out) if nil != err { t.Fatal(err) } if out.Time() != expectOutput { t.Fatal(out.Time(), expectOutput) } } func BenchmarkPayloadText(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { samplePayload.NRText() } } func TestEmptyPayloadData(t *testing.T) { // does an empty payload json blob result in an invalid payload var payload payload fixture := []byte(`{}`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from empty payload data") t.Fail() } } func TestRequiredFieldsPayloadData(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ac":"123", "ap":"456", "id":"id", "tr":"traceID", "ti":1488325987402 }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err != nil { t.Log("Expected valid payload if ty, ac, ap, id, tr, and ti are set") t.Error(err) } } func TestRequiredFieldsMissingType(t *testing.T) { var payload payload fixture := []byte(`{ "ac":"123", "ap":"456", "id":"id", "tr":"traceID", "ti":1488325987402 }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from missing Type (ty)") t.Fail() } } func TestRequiredFieldsMissingAccount(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ap":"456", "id":"id", "tr":"traceID", "ti":1488325987402 }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from missing Account (ac)") t.Fail() } } func TestRequiredFieldsMissingApp(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ac":"123", "id":"id", "tr":"traceID", "ti":1488325987402 }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from missing App (ap)") t.Fail() } } func TestRequiredFieldsMissingTimestamp(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ac":"123", "ap":"456", "tr":"traceID" }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from missing Timestamp (ti)") t.Fail() } } func TestRequiredFieldsZeroTimestamp(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ac":"123", "ap":"456", "tr":"traceID", "ti":0 }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } if err := payload.validateNewRelicData(); err == nil { t.Log("Expected error from missing Timestamp (ti)") t.Fail() } } func TestPayload_W3CTraceState(t *testing.T) { var payload payload fixture := []byte(`{ "ty":"App", "ac":"123", "ap":"456", "tr":"traceID", "ti":0, "id":"1234567890123456", "tx":"6543210987654321", "pr":0.24689, "tk":"123" }`) if err := json.Unmarshal(fixture, &payload); nil != err { t.Log("Could not marshall fixture data into payload") t.Error(err) } cases := map[string]string{ "": "123@nr=0-0-123-456-1234567890123456-6543210987654321-0-0.24689-0", "a=1,b=2": "123@nr=0-0-123-456-1234567890123456-6543210987654321-0-0.24689-0,a=1,b=2", } for k, v := range cases { payload.NonTrustedTraceState = k if act := payload.W3CTraceState(); act != v { t.Errorf("Unexpected trace state - expected %s but got %s", v, act) } } } func TestProcessTraceParent(t *testing.T) { traceParentHdr := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}, } payload, err := processTraceParent(traceParentHdr) if nil != err { t.Errorf("Unexpected error for trace parent %s: %v", traceParentHdr, err) } traceID := "4bf92f3577b34da6a3ce929d0e0e4736" if payload.TracedID != traceID { t.Errorf("Unexpected Trace ID in trace parent - expected %s, got %v", traceID, payload.TracedID) } spanID := "00f067aa0ba902b7" if payload.ID != spanID { t.Errorf("Unexpected Span ID in trace parent - expected %s, got %v", spanID, payload.ID) } if payload.Sampled != nil { t.Errorf("Expected traceparent %s sampled to be unset, but it is not", traceParentHdr) } } func TestProcessTraceParentInvalidFormat(t *testing.T) { cases := []string{ "000-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "0X-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "0-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "00-4bf92f3577b34da6a3ce929d-00f067aa0ba902b7-01", "0-4bf92f3577b34da6a3ce929d0e0e47366666666-00f067aa0ba902b7-01", "00-4bf92f3577b34da6a3ce929d0e0e4MMM-00f067aa0ba902b7-01", "00-4bf92f3577b34da6a3ce929d0e0e4736-f067aa0ba902b7-01", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b711111-01", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba9TTT7-01", "00-12345678901234567890123456789012-1234567890123456-.0", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0T", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-031", } for _, traceParent := range cases { traceParentHdr := http.Header{DistributedTraceW3CTraceParentHeader: []string{traceParent}} _, err := processTraceParent(traceParentHdr) if nil == err { t.Errorf("No error reported for trace parent %s", traceParent) } } } func TestProcessTraceState(t *testing.T) { var payload payload traceStateHdr := http.Header{ DistributedTraceW3CTraceStateHeader: []string{"190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,rojo=00f067aa0ba902b7"}, } processTraceState(traceStateHdr, "190", &payload) if payload.TrustedAccountKey != "190" { t.Errorf("Wrong trusted account key: expected 190 but got %s", payload.TrustedAccountKey) } if payload.Type != "Mobile" { t.Errorf("Wrong payload type: expected Mobile but got %s", payload.Type) } if payload.Account != "332029" { t.Errorf("Wrong account: expected 332029 but got %s", payload.Account) } if payload.App != "2827902" { t.Errorf("Wrong app ID: expected 2827902 but got %s", payload.App) } if payload.TrustedParentID != "5f474d64b9cc9b2a" { t.Errorf("Wrong Trusted Parent ID: expected 5f474d64b9cc9b2a but got %s", payload.ID) } if payload.TransactionID != "7d3efb1b173fecfa" { t.Errorf("Wrong transaction ID: expected 7d3efb1b173fecfa but got %s", payload.TransactionID) } if nil != payload.Sampled { t.Errorf("Payload sampled field was set when it should not be") } if payload.Priority != 0.0 { t.Errorf("Wrong priority: expected 0.0 but got %f", payload.Priority) } if payload.Timestamp != timestampMillis(timeFromUnixMilliseconds(1518469636035)) { t.Errorf("Wrong timestamp: expected 1518469636035 but got %v", payload.Timestamp) } } func TestExtractNRTraceStateEntry(t *testing.T) { trustedAccountID := "12345" trustedNRVal := "0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277" trustedNR := "12345@nr=" + trustedNRVal nonTrustedNR := "190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035" cases := map[string]string{ "rojo=00f06": "", // comma separator trustedNR + ",rojo=00f06,congo=t61": trustedNRVal, "congo=t61," + trustedNR + ",rojo=00f06": trustedNRVal, trustedNR + "," + nonTrustedNR: trustedNRVal, "rojo=00f06," + nonTrustedNR + ",congo=t61," + trustedNR: trustedNRVal, "rojo=00f06," + nonTrustedNR + ",congo=t61": "", // comma space separator trustedNR + ", rojo=00f06, congo=t61": trustedNRVal, "congo=t61, " + trustedNR + ", rojo=00f06": trustedNRVal, trustedNR + ", " + nonTrustedNR: trustedNRVal, "rojo=00f06, " + nonTrustedNR + ", congo=t61, " + trustedNR: trustedNRVal, "rojo=00f06, " + nonTrustedNR + ", congo=t61": "", // comma tab separator trustedNR + ",\trojo=00f06,congo=t61": trustedNRVal, "congo=t61,\t" + trustedNR + ",\trojo=00f06": trustedNRVal, trustedNR + ",\t" + nonTrustedNR: trustedNRVal, "rojo=00f06,\t" + nonTrustedNR + ",\tcongo=t61,\t" + trustedNR: trustedNRVal, "rojo=00f06,\t" + nonTrustedNR + ",\tcongo=t61": "", } for test, expected := range cases { _, _, result := parseTraceState(test, trustedAccountID) if result != expected { t.Errorf("Expected %s but got %s", expected, result) } } } func TestParseTraceState(t *testing.T) { cases := []struct { // Input trustedAccount string full string // Expect trusted string expVendors string expNonTrustState string }{ { trustedAccount: "12345", full: "12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE", trusted: "0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", expVendors: "rojo,congo", expNonTrustState: "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE", }, { trustedAccount: "12345", full: "congo=t61rcWkgMzE,12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277,rojo=00f067aa0ba902b7", trusted: "0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", expVendors: "congo,rojo", expNonTrustState: "congo=t61rcWkgMzE,rojo=00f067aa0ba902b7", }, { trustedAccount: "12345", full: "12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035", trusted: "0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", expVendors: "190@nr", expNonTrustState: "190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035", }, { trustedAccount: "12345", full: "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE,12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", trusted: "0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", expVendors: "atd@rojo,190@nr,congo", expNonTrustState: "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE", }, { trustedAccount: "12345", full: "rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,fff@congo=t61rcWkgMzE", trusted: "", expVendors: "rojo,190@nr,fff@congo", expNonTrustState: "rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,fff@congo=t61rcWkgMzE", }, { trustedAccount: "12345", full: "rojo=00f067aa0ba902b7", trusted: "", expVendors: "rojo", expNonTrustState: "rojo=00f067aa0ba902b7", }, { trustedAccount: "12345", full: "", trusted: "", expVendors: "", expNonTrustState: "", }, { trustedAccount: "12345", full: "abcdefghijklmnopqrstuvwxyz0123456789_-*/@a-z0-9_-*/= !\"#$%&'()*+-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz", trusted: "", expVendors: "abcdefghijklmnopqrstuvwxyz0123456789_-*/@a-z0-9_-*/", expNonTrustState: "abcdefghijklmnopqrstuvwxyz0123456789_-*/@a-z0-9_-*/= !\"#$%&'()*+-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz", }, } for idx, tc := range cases { vendors, state, trusted := parseTraceState(tc.full, tc.trustedAccount) if vendors != tc.expVendors { t.Errorf("testcase %d: wrong value for vendors returned, expected=%s actual=%s", idx, tc.expVendors, vendors) } if state != tc.expNonTrustState { t.Errorf("testcase %d: wrong value for state returned, expected=%s actual=%s", idx, tc.expNonTrustState, state) } if trusted != tc.trusted { t.Errorf("testcase %d: wrong value for trust returned, expected=%s actual=%s", idx, tc.trusted, trusted) } } } // Our code assumes that the keys we are using are canoncial header keys, so we should make sure // we don't accidentally change that. func TestW3CKeysAreCannoncial(t *testing.T) { if DistributedTraceW3CTraceParentHeader != http.CanonicalHeaderKey(DistributedTraceW3CTraceParentHeader) { t.Error(DistributedTraceW3CTraceParentHeader + " is not canonical") } if DistributedTraceW3CTraceStateHeader != http.CanonicalHeaderKey(DistributedTraceW3CTraceStateHeader) { t.Error(DistributedTraceW3CTraceParentHeader + " is not canonical") } } func TestTransactionIDTraceStateField(t *testing.T) { // Test that tracestate headers transactionId accepts varying vales trustKey := "33" testcases := []struct { tracestate string expect string }{ {tracestate: "33@nr=0-0-33-5-1234567890123456--0-0.0-0", expect: ""}, // TODO: support this use case which is called out specifically in the spec // {tracestate: "33@nr=0-0-33-5-1234567890123456-meatballs!-0-0.0-0", expect: "meatballs!"}, } for _, tc := range testcases { p := &payload{} h := http.Header{ DistributedTraceW3CTraceStateHeader: []string{tc.tracestate}, } processTraceState(h, trustKey, p) if p.TransactionID != tc.expect { t.Errorf("wrong transactionId gathered: expect=%s actual=%s", tc.expect, p.TransactionID) } } } func TestSpanIDTraceStateField(t *testing.T) { // Test that tracestate headers spanId accepts varying vales trustKey := "33" testcases := []struct { tracestate string expect string }{ {tracestate: "33@nr=0-0-33-5--0123456789012345-0-0.0-0", expect: ""}, // TODO: support this use case which is called out specifically in the spec // {tracestate: "33@nr=0-0-33-5-meatballs!-0123456789012345-0-0.0-0", expect: "meatballs!"}, } for _, tc := range testcases { p := &payload{} h := http.Header{ DistributedTraceW3CTraceStateHeader: []string{tc.tracestate}, } processTraceState(h, trustKey, p) if p.TrustedParentID != tc.expect { t.Errorf("wrong transactionId gathered: expect=%s actual=%s", tc.expect, p.TrustedParentID) } } } func TestVersionTraceStateField(t *testing.T) { // Test that tracestate headers version accepts varying values trustKey := "33" testcases := []struct { tracestate string expAppID string }{ { tracestate: "33@nr=0-0-33-5-0123456789012345-5432109876543210-1-0.5-123", expAppID: "5", }, { // when version is too high we still try to parse what we can tracestate: "33@nr=1-0-33-5-0123456789012345-5432109876543210-1-0.5-123-extra-fields", expAppID: "5", }, } for _, tc := range testcases { p := &payload{} h := http.Header{ DistributedTraceW3CTraceStateHeader: []string{tc.tracestate}, } processTraceState(h, trustKey, p) if p.App != tc.expAppID { t.Errorf("wrong application id set on payload: expect=%s actual=%s", tc.expAppID, p.App) } } } func TestPayloadIsSampled(t *testing.T) { p := &payload{} if s := p.isSampled(); s { t.Error(s) } p.SetSampled(true) if s := p.isSampled(); !s { t.Error(s) } p.SetSampled(false) if s := p.isSampled(); s { t.Error(s) } } func TestTraceStateSpanTxnIDs(t *testing.T) { // Test that we cover this case as stated in the spec for the tracestate // header: // Conforming agents should not require any particular format of this // string on inbound payloads beyond receiving non-delimiter characters // that are valid in a tracestate header entry. meatball! is an acceptable // spanId or transactionId. hdrs := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-meatball!-meatballs!-1-0.43771-1577830891900"}, } var support distributedTracingSupport p, err := acceptPayload(hdrs, "123", &support) if err != nil { t.Error("failure to AcceptPayload:", err) } if !support.TraceContextAcceptSuccess { t.Error("unexpected support flags", support) } if p.TrustedParentID != "meatball!" { t.Error("wrong payload ID", p.ID) } if p.TransactionID != "meatballs!" { t.Error("wrong payload TransactionID", p.TransactionID) } } func TestAcceptMultipleTraceParentHeaders(t *testing.T) { // Test that when multiple traceparent headers are received, we discard the // headers all together. From // https://github.com/w3c/trace-context/blob/3d02cfc15778ef850df9bc4e9d2740a4a2627fd5/test/test.py#L134 sup := new(distributedTracingSupport) hdrs := http.Header{ DistributedTraceW3CTraceParentHeader: []string{ "00-01234567890123456789012345678901-0123456789012345-01", "00-01234567890123456789012345678902-0123456789012346-01", }, } _, err := acceptPayload(hdrs, "123", sup) if err == nil { t.Error("error should have been returned") } } func TestAcceptW3CSuccess(t *testing.T) { hdrs := http.Header{ DistributedTraceW3CTraceParentHeader: []string{ "00-11223344556677889900aabbccddeeff-0aaabbbcccdddeee-01", }, DistributedTraceW3CTraceStateHeader: []string{ "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE,12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", }, } trustedAccountKey := "12345" support := distributedTracingSupport{} p, err := acceptPayload(hdrs, trustedAccountKey, &support) if err != nil { t.Fatal(err) } truePtr := true expect := &payload{ Type: "App", App: "41346604", Account: "1349956", TransactionID: "b28be285632bbc0a", ID: "0aaabbbcccdddeee", TracedID: "11223344556677889900aabbccddeeff", Priority: 0.24689, Sampled: &truePtr, Timestamp: timestampMillis(timeFromUnixMilliseconds(1569367663277)), TransportDuration: 0, TrustedParentID: "27ddd2d8890283b4", TracingVendors: "atd@rojo,190@nr,congo", HasNewRelicTraceInfo: true, TrustedAccountKey: "12345", NonTrustedTraceState: "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE", OriginalTraceState: "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE,12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", } if !reflect.DeepEqual(p, expect) { t.Errorf("%#v", p) } } func BenchmarkAcceptW3C(b *testing.B) { hdrs := http.Header{ DistributedTraceW3CTraceParentHeader: []string{ "00-11223344556677889900aabbccddeeff-0aaabbbcccdddeee-01", }, DistributedTraceW3CTraceStateHeader: []string{ "atd@rojo=00f067aa0ba902b7,190@nr=0-2-332029-2827902-5f474d64b9cc9b2a-7d3efb1b173fecfa---1518469636035,congo=t61rcWkgMzE,12345@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277", }, } trustedAccountKey := "12345" support := distributedTracingSupport{} b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { _, err := acceptPayload(hdrs, trustedAccountKey, &support) if err != nil { b.Fatal(err) } } } func Test_processTraceState_invalidEntry(t *testing.T) { payload := payload{} hdrs := http.Header{ DistributedTraceW3CTraceStateHeader: []string{"33@nr=-0-33-2827902-b4a146e3237b4df1-e8b91a159289ff74-1-1.23456-1518469636035"}, } err := processTraceState(hdrs, "33", &payload) if err == nil || err != errInvalidNRTraceState { t.Errorf("expected invalidNRTraceState error but got %v", err) } } go-agent-3.42.0/v3/newrelic/doc.go000066400000000000000000000005601510742411500165360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package newrelic provides instrumentation for Go applications. // // Additional documention available here: // // https://github.com/newrelic/go-agent/blob/master/GETTING_STARTED.md // // https://github.com/newrelic/go-agent/blob/master/GUIDE.md package newrelic go-agent-3.42.0/v3/newrelic/environment.go000066400000000000000000000051351510742411500203400ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "reflect" "runtime" "runtime/debug" "strings" ) // environment describes the application's environment. type environment struct { NumCPU int `env:"runtime.NumCPU"` Compiler string `env:"runtime.Compiler"` GOARCH string `env:"runtime.GOARCH"` GOOS string `env:"runtime.GOOS"` Version string `env:"runtime.Version"` Modules []string `env:"Modules"` } var ( // sampleEnvironment is useful for testing. sampleEnvironment = environment{ Compiler: "comp", GOARCH: "arch", GOOS: "goos", Version: "vers", NumCPU: 8, } ) // newEnvironment returns a new Environment. func newEnvironment(c *config) environment { return environment{ Compiler: runtime.Compiler, GOARCH: runtime.GOARCH, GOOS: runtime.GOOS, Version: runtime.Version(), NumCPU: runtime.NumCPU(), Modules: getDependencyModuleList(c), } } // indended for testing purposes. This just returns the formatted // modules subject to the user's filtering rules. func injectDependencyModuleList(c *config, modules []*debug.Module) []string { var modList []string if c != nil && c.ModuleDependencyMetrics.Enabled { for _, module := range modules { if module != nil && includeModule(module.Path, c.ModuleDependencyMetrics.IgnoredPrefixes) { modList = append(modList, fmt.Sprintf("%s(%s)", module.Path, module.Version)) } } } return modList } func getDependencyModuleList(c *config) []string { var modList []string if c != nil && c.ModuleDependencyMetrics.Enabled { info, ok := debug.ReadBuildInfo() if info != nil && ok { for _, module := range info.Deps { if module != nil && includeModule(module.Path, c.ModuleDependencyMetrics.IgnoredPrefixes) { modList = append(modList, fmt.Sprintf("%s(%s)", module.Path, module.Version)) } } } } return modList } func includeModule(name string, ignoredModulePrefixes []string) bool { for _, excluded := range ignoredModulePrefixes { if strings.HasPrefix(name, excluded) { return false } } return true } // MarshalJSON prepares Environment JSON in the format expected by the collector // during the connect command. func (e environment) MarshalJSON() ([]byte, error) { var arr [][]interface{} val := reflect.ValueOf(e) numFields := val.NumField() arr = make([][]interface{}, numFields) for i := 0; i < numFields; i++ { v := val.Field(i) t := val.Type().Field(i).Tag.Get("env") arr[i] = []interface{}{ t, v.Interface(), } } return json.Marshal(arr) } go-agent-3.42.0/v3/newrelic/environment_test.go000066400000000000000000000113421510742411500213740ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "regexp" "runtime" "runtime/debug" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestMarshalEnvironment(t *testing.T) { js, err := json.Marshal(&sampleEnvironment) if nil != err { t.Fatal(err) } expect := internal.CompactJSONString(`[ ["runtime.NumCPU",8], ["runtime.Compiler","comp"], ["runtime.GOARCH","arch"], ["runtime.GOOS","goos"], ["runtime.Version","vers"], ["Modules",null]]`) if string(js) != expect { t.Fatal(string(js)) } } func TestEnvironmentFields(t *testing.T) { env := newEnvironment(nil) if env.Compiler != runtime.Compiler { t.Error(env.Compiler, runtime.Compiler) } if env.GOARCH != runtime.GOARCH { t.Error(env.GOARCH, runtime.GOARCH) } if env.GOOS != runtime.GOOS { t.Error(env.GOOS, runtime.GOOS) } if env.Version != runtime.Version() { t.Error(env.Version, runtime.Version()) } if env.NumCPU != runtime.NumCPU() { t.Error(env.NumCPU, runtime.NumCPU()) } if env.Modules != nil { t.Error(env.Modules, nil) } } func TestModuleDependency(t *testing.T) { cfg := config{Config: defaultConfig()} // check that the default is to be enabled if !cfg.ModuleDependencyMetrics.Enabled { t.Error("MDM should be enabled, was", cfg.ModuleDependencyMetrics.Enabled) } // if disabled, we shouldn't get any data cfg.ModuleDependencyMetrics.Enabled = false env := newEnvironment(&cfg) if env.Modules != nil && len(env.Modules) != 0 { t.Error("MDM module list not empty:", env.Modules) } // enabled, and we should see our list of modules reported. // first, get the list of modules we should expect to see. // of course, we can't do that from a unit test, so we'll mock up a set // of modules to at least check that the various options work. expectedModules := make(map[string]*debug.Module) mockedModuleList := []*debug.Module{ {Path: "example/path/to/module", Version: "v1.2.3"}, {Path: "github.com/another/module", Version: "v0.1.2"}, {Path: "some/development/module", Version: "(develop)"}, } for _, module := range mockedModuleList { expectedModules[module.Path] = module } cfg.ModuleDependencyMetrics.Enabled = true env = newEnvironment(&cfg) env.Modules = injectDependencyModuleList(&cfg, mockedModuleList) checkModuleListsMatch(t, expectedModules, env.Modules, "full module list") // try to elide some modules now cfg.ModuleDependencyMetrics.IgnoredPrefixes = []string{"github.com"} env = newEnvironment(&cfg) env.Modules = injectDependencyModuleList(&cfg, mockedModuleList) delete(expectedModules, "github.com/another/module") checkModuleListsMatch(t, expectedModules, env.Modules, "reduced module list") // more... cfg.ModuleDependencyMetrics.IgnoredPrefixes = []string{"github.com", "exam"} env = newEnvironment(&cfg) env.Modules = injectDependencyModuleList(&cfg, mockedModuleList) delete(expectedModules, "example/path/to/module") checkModuleListsMatch(t, expectedModules, env.Modules, "reduced module list") } func checkModuleListsMatch(t *testing.T, expected map[string]*debug.Module, actual []string, message string) { if expected == nil { t.Error(message, "expected list is nil") } if len(expected) > 0 && actual == nil { t.Error(message, "actual list is nil") } if len(expected) != len(actual) { t.Error(message, "actual list has", len(actual), "module(s) but expected", len(expected)) } modulePattern := regexp.MustCompile(`^(.+?)\((.+)\)$`) checked := make(map[string]bool) for path, _ := range expected { checked[path] = false } for i, actualName := range actual { matches := modulePattern.FindStringSubmatch(actualName) if matches == nil || len(matches) != 3 { t.Errorf("%s: actual module element #%d could not be parsed: \"%v\"", message, i, actualName) continue } if module, present := expected[matches[1]]; present { if matches[1] != module.Path { t.Errorf("%s: actual module element #%d \"%v\" mismatch to path \"%v\" which really shouldn't be possible", message, i, matches[1], module.Path) continue } if matches[2] != module.Version { t.Errorf("%s: actual module element #%d \"%v\" version \"%v\" mismatch to expected version \"%v\"", message, i, matches[1], matches[2], module.Version) continue } if checked[matches[1]] { t.Errorf("%s: actual module element #%d \"%v\" was already seen earlier in the module list", message, i, matches[1]) continue } checked[matches[1]] = true } else { t.Errorf("%s: actual module element #%d \"%v\" unexpected", message, i, matches[1]) } } for expectedName, wasChecked := range checked { if !wasChecked { t.Errorf("%s: did not see expected module \"%v\"", message, expectedName) } } } go-agent-3.42.0/v3/newrelic/error_events.go000066400000000000000000000036601510742411500205120ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "time" ) // MarshalJSON is used for testing. func (e *errorEvent) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, 256)) e.WriteJSON(buf) return buf.Bytes(), nil } // WriteJSON prepares JSON in the format expected by the collector. // https://source.datanerd.us/agents/agent-specs/blob/master/Error-Events.md func (e *errorEvent) WriteJSON(buf *bytes.Buffer) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('[') buf.WriteByte('{') w.stringField("type", "TransactionError") w.stringField("error.class", e.Klass) w.stringField("error.message", e.Msg) w.intField("timestamp", timeToIntMillis(e.When)) w.stringField("transactionName", e.FinalName) if e.SpanID != "" { w.stringField("spanId", e.SpanID) } if e.Expect { w.boolField(expectErrorAttr, true) } sharedTransactionIntrinsics(&e.txnEvent, &w) sharedBetterCATIntrinsics(&e.txnEvent, &w) buf.WriteByte('}') buf.WriteByte(',') userAttributesJSON(e.Attrs, buf, destError, e.errorData.ExtraAttributes) buf.WriteByte(',') if e.ErrorGroup != "" { agentAttributesJSON(e.Attrs, buf, destError, map[string]string{AttributeErrorGroupName: e.ErrorGroup}) } else { agentAttributesJSON(e.Attrs, buf, destError) } buf.WriteByte(']') } type errorEvents struct { *analyticsEvents } func newErrorEvents(max int) *errorEvents { return &errorEvents{ analyticsEvents: newAnalyticsEvents(max), } } func (events *errorEvents) Add(e *errorEvent, p priority) { events.addEvent(analyticsEvent{p, e}) } func (events *errorEvents) MergeIntoHarvest(h *harvest) { h.ErrorEvents.mergeFailed(events.analyticsEvents) } func (events *errorEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return events.CollectorJSON(agentRunID) } func (events *errorEvents) EndpointMethod() string { return cmdErrorEvents } go-agent-3.42.0/v3/newrelic/error_events_test.go000066400000000000000000000125271510742411500215530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "testing" "time" ) func testErrorEventJSON(t testing.TB, e *errorEvent, expect string) { js, err := json.Marshal(e) if nil != err { t.Error(err) return } expect = compactJSONString(expect) // Type assertion to support early Go versions. if h, ok := t.(interface { Helper() }); ok { h.Helper() } actual := string(js) if expect != actual { t.Errorf("\nexpect=%s\nactual=%s\n", expect, actual) } } var ( sampleErrorData = errorData{ Klass: "*errors.errorString", Msg: "hello", When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), } ) func TestErrorEventMarshal(t *testing.T) { testErrorEventJSON(t, &errorEvent{ errorData: sampleErrorData, txnEvent: txnEvent{ FinalName: "myName", Duration: 3 * time.Second, Attrs: nil, BetterCAT: betterCAT{ Enabled: true, Priority: 0.5, TxnID: "txn-guid-id", TraceID: "trace-id", }, }, }, `[ { "type":"TransactionError", "error.class":"*errors.errorString", "error.message":"hello", "timestamp":1417136460000, "transactionName":"myName", "duration":3, "guid":"txn-guid-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, {}, {} ]`) // Many error event intrinsics are shared with txn events using sharedEventIntrinsics: See // the txn event tests. } func TestErrorEventMarshalOldCAT(t *testing.T) { testErrorEventJSON(t, &errorEvent{ errorData: sampleErrorData, txnEvent: txnEvent{ FinalName: "myName", Duration: 3 * time.Second, Attrs: nil, BetterCAT: betterCAT{ Enabled: false, }, }, }, `[ { "type":"TransactionError", "error.class":"*errors.errorString", "error.message":"hello", "timestamp":1417136460000, "transactionName":"myName", "duration":3 }, {}, {} ]`) // Many error event intrinsics are shared with txn events using sharedEventIntrinsics: See // the txn event tests. } func TestErrorEventAttributes(t *testing.T) { aci := config{Config: defaultConfig()} aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, "zap") aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, AttributeHostDisplayName) cfg := createAttributeConfig(aci, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeHostDisplayName, "exclude me", nil) attr.Agent.Add(AttributeRequestMethod, "GET", nil) addUserAttribute(attr, "zap", 123, destAll) addUserAttribute(attr, "zip", 456, destAll) testErrorEventJSON(t, &errorEvent{ errorData: sampleErrorData, txnEvent: txnEvent{ FinalName: "myName", Duration: 3 * time.Second, Attrs: attr, BetterCAT: betterCAT{ Enabled: true, Priority: 0.5, TxnID: "txn-guid-id", TraceID: "trace-id", }, }, }, `[ { "type":"TransactionError", "error.class":"*errors.errorString", "error.message":"hello", "timestamp":1417136460000, "transactionName":"myName", "duration":3, "guid":"txn-guid-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, { "zip":456 }, { "request.method":"GET" } ]`) } func TestErrorEventAttributesOldCAT(t *testing.T) { aci := config{Config: defaultConfig()} aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, "zap") aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, AttributeHostDisplayName) cfg := createAttributeConfig(aci, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeHostDisplayName, "exclude me", nil) attr.Agent.Add(AttributeRequestMethod, "GET", nil) addUserAttribute(attr, "zap", 123, destAll) addUserAttribute(attr, "zip", 456, destAll) testErrorEventJSON(t, &errorEvent{ errorData: sampleErrorData, txnEvent: txnEvent{ FinalName: "myName", Duration: 3 * time.Second, Attrs: attr, BetterCAT: betterCAT{ Enabled: false, }, }, }, `[ { "type":"TransactionError", "error.class":"*errors.errorString", "error.message":"hello", "timestamp":1417136460000, "transactionName":"myName", "duration":3 }, { "zip":456 }, { "request.method":"GET" } ]`) } func TestErrorEventMarshalWithInboundCaller(t *testing.T) { e := txnEvent{ FinalName: "myName", Duration: 3 * time.Second, Attrs: nil, } e.BetterCAT.Enabled = true e.BetterCAT.TraceID = "trip-id" e.BetterCAT.TransportType = "HTTP" e.BetterCAT.Inbound = &payload{ Type: "Browser", App: "caller-app", Account: "caller-account", ID: "caller-id", TransactionID: "caller-parent-id", TracedID: "trip-id", TransportDuration: 2 * time.Second, HasNewRelicTraceInfo: true, } testErrorEventJSON(t, &errorEvent{ errorData: sampleErrorData, txnEvent: e, }, `[ { "type":"TransactionError", "error.class":"*errors.errorString", "error.message":"hello", "timestamp":1417136460000, "transactionName":"myName", "duration":3, "parent.type": "Browser", "parent.app": "caller-app", "parent.account": "caller-account", "parent.transportDuration": 2, "parent.transportType": "HTTP", "guid":"", "traceId":"trip-id", "priority":0.000000, "sampled":false }, {}, {} ]`) } go-agent-3.42.0/v3/newrelic/error_group.go000066400000000000000000000116111510742411500203350ustar00rootroot00000000000000package newrelic import "time" const ( // The error class for panics PanicErrorClass = panicErrorKlass ) // ErrorInfo contains info for user defined callbacks that are relevant to an error. // All fields are either safe to access copies of internal agent data, or protected from direct // access with methods and can not manipulate or distort any agent data. type ErrorInfo struct { errAttributes map[string]interface{} txnAttributes *attributes stackTrace stackTrace // TransactionName is the formatted name of a transaction that is equivilent to how it appears in // the New Relic UI. For example, user defined transactions will be named `OtherTransaction/Go/yourTxnName`. TransactionName string // Error contains the raw error object that the agent collected. // // Not all errors collected by the system are collected as // error objects, like web/http errors and panics. // In these cases, Error will be nil, but details will be captured in // the Message and Class fields. Error error // Time Occured is the time.Time when the error was noticed by the go agent TimeOccured time.Time // Message will always be populated by a string message describing an error Message string // Class is a string containing the New Relic error class. // // If an error implements errorClasser, its value will be derived from that. // Otherwise, it will be derived from the way the error was // collected by the agent. For http errors, this will be the // error number. Panics will be the constant value `newrelic.PanicErrorClass`. // If no errorClass was defined, this will be reflect.TypeOf() the root // error object, which is commonly `*errors.errorString`. Class string // Expected is true if the error was expected by the go agent Expected bool } // GetTransactionUserAttribute safely looks up a user attribute by string key from the parent transaction // of an error. This function will return the attribute vaue as an interface{}, and a bool indicating whether the // key was found in the attribute map. If the key was not found, then the return will be (nil, false). func (e *ErrorInfo) GetTransactionUserAttribute(attribute string) (interface{}, bool) { a, b := e.txnAttributes.user[attribute] if b { return a.value, b } return nil, b } // GetErrorAttribute safely looks up an error attribute by string key. The value of the attribute will be returned // as an interface{}, and a bool indicating whether the key was found in the attribute map. If no matching key was // found, the return will be (nil, false). func (e *ErrorInfo) GetErrorAttribute(attribute string) (interface{}, bool) { a, b := e.errAttributes[attribute] return a, b } // GetStackTraceFrames returns a slice of StacktraceFrame objects containing up to 100 lines of stack trace // data gathered from the Go runtime. Calling this function may be expensive since it allocates and // populates a new slice with stack trace data, and should be called only when needed. func (e *ErrorInfo) GetStackTraceFrames() []StacktraceFrame { return e.stackTrace.frames() } // GetRequestURI returns the URI of the http request made during the parent transaction of this error. If no web request occured, // this will return an empty string. func (e *ErrorInfo) GetRequestURI() string { val, ok := e.txnAttributes.Agent[AttributeRequestURI] if !ok { return "" } return val.stringVal } // GetRequestMethod will return the HTTP method used to make a web request if one occured during the parent transaction // of this error. If no web request occured, then an empty string will be returned. func (e *ErrorInfo) GetRequestMethod() string { val, ok := e.txnAttributes.Agent[AttributeRequestMethod] if !ok { return "" } return val.stringVal } // GetHttpResponseCode will return the HTTP response code that resulted from the web request made in the parent transaction of // this error. If no web request occured, then an empty string will be returned. func (e *ErrorInfo) GetHttpResponseCode() string { val, ok := e.txnAttributes.Agent[AttributeResponseCode] if !ok { return "" } code := val.stringVal if code != "" { return code } val, ok = e.txnAttributes.Agent[AttributeResponseCodeDeprecated] if !ok { return "" } return val.stringVal } // GetUserID will return the User ID set for the parent transaction of this error. It will return empty string // if none was set. func (e *ErrorInfo) GetUserID() string { val, ok := e.txnAttributes.Agent[AttributeUserID] if !ok { return "" } return val.stringVal } // ErrorGroupCallback is a user defined callback function that takes an error as an input // and returns a string that will be applied to an error to put it in an error group. // // If no error group is identified for a given error, this function should return an empty string. // // If an ErrorGroupCallbeck is defined, it will be executed against every error the go agent notices that // is not ignored. type ErrorGroupCallback func(ErrorInfo) string go-agent-3.42.0/v3/newrelic/errors.go000066400000000000000000000036201510742411500173050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic // stackTracer can be implemented by errors to provide a stack trace when using // Transaction.NoticeError. type stackTracer interface { StackTrace() []uintptr } // errorClasser can be implemented by errors to provide a custom class when // using Transaction.NoticeError. type errorClasser interface { ErrorClass() string } // errorAttributer can be implemented by errors to provide extra context when // using Transaction.NoticeError. type errorAttributer interface { ErrorAttributes() map[string]interface{} } // Error is an error designed for use with Transaction.NoticeError. It allows // direct control over the recorded error's message, class, stacktrace, and // attributes. type Error struct { // Message is the error message which will be returned by the Error() // method. Message string // Class indicates how the error may be aggregated. Class string // Attributes are attached to traced errors and error events for // additional context. These attributes are validated just like those // added to Transaction.AddAttribute. Attributes map[string]interface{} // Stack is the stack trace. Assign this field using NewStackTrace, // or leave it nil to indicate that Transaction.NoticeError should // generate one. Stack []uintptr } // NewStackTrace generates a stack trace for the newrelic.Error struct's Stack // field. func NewStackTrace() []uintptr { st := getStackTrace() return []uintptr(st) } func (e Error) Error() string { return e.Message } // ErrorClass returns the error's class. func (e Error) ErrorClass() string { return e.Class } // ErrorAttributes returns the error's extra attributes. func (e Error) ErrorAttributes() map[string]interface{} { return e.Attributes } // StackTrace returns the error's stack. func (e Error) StackTrace() []uintptr { return e.Stack } go-agent-3.42.0/v3/newrelic/errors_from_internal.go000066400000000000000000000136211510742411500222260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "fmt" "net/http" "strconv" "time" "github.com/newrelic/go-agent/v3/internal/jsonx" ) const ( // panicErrorKlass is the error klass used for errors generated by // recovering panics in txn.End. panicErrorKlass = "panic" ) func panicValueMsg(v interface{}) string { switch val := v.(type) { case error: return val.Error() default: return fmt.Sprintf("%v", v) } } // txnErrorFromPanic creates a new TxnError from a panic. func txnErrorFromPanic(now time.Time, v interface{}) errorData { return errorData{ When: now, Msg: panicValueMsg(v), Klass: panicErrorKlass, } } // txnErrorFromResponseCode creates a new TxnError from an http response code. func txnErrorFromResponseCode(now time.Time, code int) errorData { codeStr := strconv.Itoa(code) msg := http.StatusText(code) if msg == "" { // Use a generic message if the code was not an http code // to support gRPC. msg = "response code " + codeStr } return errorData{ When: now, Msg: msg, Klass: codeStr, } } // errorData contains the information about a recorded error. type errorData struct { When time.Time Stack stackTrace RawError error ExtraAttributes map[string]interface{} ErrorGroup string Msg string Klass string SpanID string Expect bool } // txnError combines error data with information about a transaction. txnError is used for // both error events and traced errors. type txnError struct { errorData txnEvent } // errorEvent and tracedError are separate types so that error events and traced errors can have // different WriteJSON methods. type errorEvent txnError type tracedError txnError // txnErrors is a set of errors captured in a Transaction. type txnErrors []*errorData // NewTxnErrors returns a new empty txnErrors. func newTxnErrors(max int) txnErrors { return make([]*errorData, 0, max) } // Add adds a TxnError. func (errors *txnErrors) Add(e errorData) { if len(*errors) < cap(*errors) { *errors = append(*errors, &e) } } func (h *tracedError) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('[') jsonx.AppendFloat(buf, timeToFloatMilliseconds(h.When)) buf.WriteByte(',') jsonx.AppendString(buf, h.FinalName) buf.WriteByte(',') jsonx.AppendString(buf, h.Msg) buf.WriteByte(',') jsonx.AppendString(buf, h.Klass) buf.WriteByte(',') buf.WriteByte('{') buf.WriteString(`"agentAttributes"`) buf.WriteByte(':') agentAttributesJSON(h.Attrs, buf, destError) buf.WriteByte(',') buf.WriteString(`"userAttributes"`) buf.WriteByte(':') userAttributesJSON(h.Attrs, buf, destError, h.errorData.ExtraAttributes) buf.WriteByte(',') buf.WriteString(`"intrinsics"`) buf.WriteByte(':') intrinsicsJSON(&h.txnEvent, buf, h.errorData.Expect) if nil != h.Stack { buf.WriteByte(',') buf.WriteString(`"stack_trace"`) buf.WriteByte(':') h.Stack.WriteJSON(buf) } buf.WriteByte('}') buf.WriteByte(',') jsonx.AppendString(buf, h.txnEvent.TxnID) buf.WriteByte(']') } // MarshalJSON is used for testing. func (h *tracedError) MarshalJSON() ([]byte, error) { buf := &bytes.Buffer{} h.WriteJSON(buf) return buf.Bytes(), nil } type harvestErrors []*tracedError func newHarvestErrors(max int) harvestErrors { return make([]*tracedError, 0, max) } // mergeTxnErrors merges a transaction's errors into the harvest's errors. func mergeTxnErrors(errors *harvestErrors, errs txnErrors, txnEvent txnEvent, hs *highSecuritySettings) { for _, e := range errs { if len(*errors) == cap(*errors) { return } e.scrubErrorForHighSecurity(hs) *errors = append(*errors, &tracedError{ txnEvent: txnEvent, errorData: *e, }) } } func (errors harvestErrors) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { if len(errors) == 0 { return nil, nil } estimate := 1024 * len(errors) buf := bytes.NewBuffer(make([]byte, 0, estimate)) buf.WriteByte('[') jsonx.AppendString(buf, agentRunID) buf.WriteByte(',') buf.WriteByte('[') for i, e := range errors { if i > 0 { buf.WriteByte(',') } e.WriteJSON(buf) } buf.WriteByte(']') buf.WriteByte(']') return buf.Bytes(), nil } func (errors harvestErrors) MergeIntoHarvest(h *harvest) {} func (errors harvestErrors) EndpointMethod() string { return cmdErrorData } // applyErrorGroup applies the error group callback function to an errorData object. It will either consume the txn object // or the txnEvent in that order. If both are nil, nothing will happen. func (errData *errorData) applyErrorGroup(txnEvent *txnEvent) { if txnEvent == nil || txnEvent.errGroupCallback == nil { return } errorInfo := ErrorInfo{ txnAttributes: txnEvent.Attrs, TransactionName: txnEvent.FinalName, errAttributes: errData.ExtraAttributes, stackTrace: errData.Stack, Error: errData.RawError, TimeOccured: errData.When, Message: errData.Msg, Class: errData.Klass, Expected: errData.Expect, } // If a user defined an error group callback function, execute it to generate the error group string. errGroup := txnEvent.errGroupCallback(errorInfo) if errGroup != "" { errData.ErrorGroup = errGroup } } type highSecuritySettings struct { enabled bool allowRawExceptionMessages bool } func (errData *errorData) scrubErrorForHighSecurity(hs *highSecuritySettings) { if hs == nil { return } //txn.Config.HighSecurity if hs.enabled { errData.Msg = highSecurityErrorMsg } //!txn.Reply.SecurityPolicies.AllowRawExceptionMessages.Enabled() if !hs.allowRawExceptionMessages { errData.Msg = securityPolicyErrorMsg } } func scrubbedErrorMessage(msg string, txn *txn) string { if txn == nil { return msg } if txn.Config.HighSecurity { return highSecurityErrorMsg } if !txn.Reply.SecurityPolicies.AllowRawExceptionMessages.Enabled() { return securityPolicyErrorMsg } return msg } go-agent-3.42.0/v3/newrelic/errors_test.go000066400000000000000000000213751510742411500203530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "errors" "testing" "time" ) var ( emptyStackTrace = make([]uintptr, 0) ) func testExpectedJSON(t testing.TB, expect string, actual string) { // Type assertion to support early Go versions. if h, ok := t.(interface { Helper() }); ok { h.Helper() } compactExpect := compactJSONString(expect) if compactExpect != actual { t.Errorf("\nexpect=%s\nactual=%s\n", compactExpect, actual) } } func TestErrorNoCAT(t *testing.T) { he := &tracedError{ errorData: errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Stack: emptyStackTrace, Msg: "my_msg", Klass: "my_class", }, txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: nil, TxnID: "txn-guid-id", BetterCAT: betterCAT{ Enabled: false, }, TotalTime: 2 * time.Second, }, } js, err := json.Marshal(he) if nil != err { t.Error(err) } expect := ` [ 1.41713646e+12, "my_txn_name", "my_msg", "my_class", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2 }, "stack_trace":[] }, "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } func TestErrorTraceMarshal(t *testing.T) { he := &tracedError{ errorData: errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Stack: emptyStackTrace, Msg: "my_msg", Klass: "my_class", }, txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: nil, TxnID: "txn-guid-id", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, TotalTime: 2 * time.Second, }, } js, err := json.Marshal(he) if nil != err { t.Error(err) } expect := ` [ 1.41713646e+12, "my_txn_name", "my_msg", "my_class", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, "stack_trace":[] }, "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } func TestErrorTraceMarshalOldCAT(t *testing.T) { he := &tracedError{ errorData: errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Stack: emptyStackTrace, Msg: "my_msg", Klass: "my_class", }, txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: nil, BetterCAT: betterCAT{ Enabled: false, }, TotalTime: 2 * time.Second, TxnID: "txn-guid-id", }, } js, err := json.Marshal(he) if nil != err { t.Error(err) } expect := ` [ 1.41713646e+12, "my_txn_name", "my_msg", "my_class", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2 }, "stack_trace":[] }, "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } func TestErrorTraceAttributes(t *testing.T) { aci := config{Config: defaultConfig()} aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, "zap") aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, AttributeHostDisplayName) cfg := createAttributeConfig(aci, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeHostDisplayName, "exclude me", nil) attr.Agent.Add(AttributeRequestURI, "my_request_uri", nil) addUserAttribute(attr, "zap", 123, destAll) addUserAttribute(attr, "zip", 456, destAll) he := &tracedError{ errorData: errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Stack: nil, Msg: "my_msg", Klass: "my_class", }, txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: attr, TxnID: "txn-id", BetterCAT: betterCAT{ Enabled: true, Priority: 0.5, TraceID: "trace-id", TxnID: "txn-id", }, TotalTime: 2 * time.Second, }, } js, err := json.Marshal(he) if nil != err { t.Error(err) } expect := ` [ 1.41713646e+12, "my_txn_name", "my_msg", "my_class", { "agentAttributes":{"request.uri":"my_request_uri"}, "userAttributes":{"zip":456}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } }, "txn-id" ]` testExpectedJSON(t, expect, string(js)) } func TestErrorTraceAttributesOldCAT(t *testing.T) { aci := config{Config: defaultConfig()} aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, "zap") aci.ErrorCollector.Attributes.Exclude = append(aci.ErrorCollector.Attributes.Exclude, AttributeHostDisplayName) cfg := createAttributeConfig(aci, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeHostDisplayName, "exclude me", nil) attr.Agent.Add(AttributeRequestURI, "my_request_uri", nil) addUserAttribute(attr, "zap", 123, destAll) addUserAttribute(attr, "zip", 456, destAll) he := &tracedError{ errorData: errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Stack: nil, Msg: "my_msg", Klass: "my_class", }, txnEvent: txnEvent{ TxnID: "txn-guid-id", FinalName: "my_txn_name", Attrs: attr, BetterCAT: betterCAT{ Enabled: false, }, TotalTime: 2 * time.Second, }, } js, err := json.Marshal(he) if nil != err { t.Error(err) } expect := ` [ 1.41713646e+12, "my_txn_name", "my_msg", "my_class", { "agentAttributes":{"request.uri":"my_request_uri"}, "userAttributes":{"zip":456}, "intrinsics":{ "totalTime":2 } }, "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } func TestErrorsLifecycle(t *testing.T) { ers := newTxnErrors(5) when := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) ers.Add(txnErrorFromResponseCode(when, 15)) ers.Add(txnErrorFromResponseCode(when, 400)) ers.Add(txnErrorFromPanic(when, errors.New("oh no panic"))) ers.Add(txnErrorFromPanic(when, 123)) ers.Add(txnErrorFromPanic(when, 123)) he := newHarvestErrors(4) mergeTxnErrors(&he, ers, txnEvent{ FinalName: "txnName", Attrs: nil, TxnID: "txn-id", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, TotalTime: 2 * time.Second, }, nil) js, err := he.Data("agentRunID", time.Now()) if nil != err { t.Error(err) } expect := compactJSONString(` [ "agentRunID", [ [ 1.41713646e+12, "txnName", "response code 15", "15", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } }, "txn-id" ], [ 1.41713646e+12, "txnName", "Bad Request", "400", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } }, "txn-id" ], [ 1.41713646e+12, "txnName", "oh no panic", "panic", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } }, "txn-id" ], [ 1.41713646e+12, "txnName", "123", "panic", { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":2, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } }, "txn-id" ] ] ]`) if string(js) != expect { t.Error(string(js), "expect: ", expect) } } func BenchmarkErrorsJSON(b *testing.B) { when := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) max := 20 ers := newTxnErrors(max) for i := 0; i < max; i++ { ers.Add(errorData{ When: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Msg: "error message", Klass: "error class", }) } cfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeRequestMethod, "GET", nil) addUserAttribute(attr, "zip", 456, destAll) he := newHarvestErrors(max) mergeTxnErrors(&he, ers, txnEvent{ FinalName: "WebTransaction/Go/hello", Attrs: attr, }, nil) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { js, err := he.Data("agentRundID", when) if nil != err || nil == js { b.Fatal(err, js) } } } go-agent-3.42.0/v3/newrelic/examples_test.go000066400000000000000000000305241510742411500206510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic_test import ( "context" "errors" "fmt" "io" "net/http" "net/url" "os" "time" "github.com/newrelic/go-agent/v3/newrelic" ) func Example() { // Create your application using your preferred app name, license key, and // any other configuration options. app, err := newrelic.NewApplication( newrelic.ConfigAppName("Example Application"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), newrelic.ConfigCodeLevelMetricsEnabled(false), newrelic.ConfigDebugLogger(os.Stdout), ) if nil != err { fmt.Println(err) os.Exit(1) } // Now you can use the Application to collect data! Create transactions // to time inbound requests or background tasks. You can start and stop // transactions directly using Application.StartTransaction and // Transaction.End. func() { txn := app.StartTransaction("myTask") defer txn.End() // Do some work time.Sleep(time.Second) }() // WrapHandler and WrapHandleFunc make it easy to instrument inbound // web requests handled by the http standard library without calling // Application.StartTransaction. Popular framework instrumentation // packages exist in the v3/integrations directory. http.HandleFunc(newrelic.WrapHandleFunc(app, "", func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "this is the index page") })) helloHandler := func(w http.ResponseWriter, req *http.Request) { // WrapHandler and WrapHandleFunc add the transaction to the // inbound request's context. Access the transaction using // FromContext to add attributes, create segments, and notice. // errors. txn := newrelic.FromContext(req.Context()) func() { // Segments help you understand where the time in your // transaction is being spent. You can use them to time // functions or arbitrary blocks of code. defer txn.StartSegment("helperFunction").End() }() io.WriteString(w, "hello world") } http.HandleFunc(newrelic.WrapHandleFunc(app, "/hello", helloHandler)) http.ListenAndServe(":8000", nil) } func currentTransaction() *newrelic.Transaction { return nil } var txn *newrelic.Transaction func ExampleNewRoundTripper() { client := &http.Client{} // The http.RoundTripper returned by NewRoundTripper instruments all // requests done by this client with external segments. client.Transport = newrelic.NewRoundTripper(client.Transport) request, _ := http.NewRequest("GET", "https://example.com", nil) // Be sure to add the current Transaction to each request's context so // the Transport has access to it. txn := currentTransaction() request = newrelic.RequestWithTransactionContext(request, txn) client.Do(request) } func getApp() *newrelic.Application { return nil } func ExampleBrowserTimingHeader() { handler := func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "") // The New Relic browser javascript should be placed as high in the // HTML as possible. We suggest including it immediately after the // opening tag and any tags. txn := newrelic.FromContext(req.Context()) hdr := txn.BrowserTimingHeader() // BrowserTimingHeader() will always return a header whose methods can // be safely called. if js := hdr.WithTags(); js != nil { w.Write(js) } io.WriteString(w, "browser header page") } http.HandleFunc(newrelic.WrapHandleFunc(getApp(), "/browser", handler)) http.ListenAndServe(":8000", nil) } func ExampleDatastoreSegment() { txn := currentTransaction() ds := &newrelic.DatastoreSegment{ StartTime: txn.StartSegmentNow(), // Product, Collection, and Operation are the primary metric // aggregation fields which we encourage you to populate. Product: newrelic.DatastoreMySQL, Collection: "users_table", Operation: "SELECT", } // your database call here ds.End() } func ExampleMessageProducerSegment() { txn := currentTransaction() seg := &newrelic.MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: newrelic.MessageExchange, DestinationName: "myExchange", } // add message to queue here seg.End() } func ExampleError() { txn := currentTransaction() username := "gopher" e := fmt.Errorf("error unable to login user %s", username) // txn.NoticeError(newrelic.Error{...}) instead of txn.NoticeError(e) // allows more control over error fields. Class is how errors are // aggregated and Attributes are added to the error event and error // trace. txn.NoticeError(newrelic.Error{ Message: e.Error(), Class: "LoginError", Attributes: map[string]interface{}{ "username": username, }, }) } func ExampleExternalSegment() { txn := currentTransaction() client := &http.Client{} request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) response, _ := client.Do(request) segment.Response = response segment.End() } // StartExternalSegment is the recommend way of creating ExternalSegments. If // you don't have access to an http.Request, however, you may create an // ExternalSegment and control the URL manually. func ExampleExternalSegment_url() { txn := currentTransaction() segment := newrelic.ExternalSegment{ StartTime: txn.StartSegmentNow(), // URL is parsed using url.Parse so it must include the protocol // scheme (eg. "http://"). The host of the URL is used to // create metrics. Change the host to alter aggregation. URL: "http://www.example.com", } http.Get("http://www.example.com") segment.End() } func ExampleStartExternalSegment() { txn := currentTransaction() client := &http.Client{} request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) response, _ := client.Do(request) segment.Response = response segment.End() } func ExampleStartExternalSegment_context() { txn := currentTransaction() request, _ := http.NewRequest("GET", "https://www.example.com", nil) // If the transaction is added to the request's context then it does not // need to be provided as a parameter to StartExternalSegment. request = newrelic.RequestWithTransactionContext(request, txn) segment := newrelic.StartExternalSegment(nil, request) client := &http.Client{} response, _ := client.Do(request) segment.Response = response segment.End() } func doSendRequest(*http.Request) int { return 418 } // Use ExternalSegment.SetStatusCode when you do not have access to an // http.Response and still want to record the response status code. func ExampleExternalSegment_SetStatusCode() { txn := currentTransaction() request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) statusCode := doSendRequest(request) segment.SetStatusCode(statusCode) segment.End() } func ExampleTransaction_SetWebRequest() { app := getApp() txn := app.StartTransaction("My-Transaction") txn.SetWebRequest(newrelic.WebRequest{ Header: http.Header{}, URL: &url.URL{Path: "path"}, Method: "GET", Transport: newrelic.TransportHTTP, }) } func ExampleTransaction_SetWebRequestHTTP() { app := getApp() inboundRequest, _ := http.NewRequest("GET", "https://example.com", nil) txn := app.StartTransaction("My-Transaction") // Mark transaction as a web transaction, record attributes based on the // inbound request, and read any available distributed tracing headers. txn.SetWebRequestHTTP(inboundRequest) } // Sometimes there is no inbound request, but you may still wish to set a // Transaction as a web request. Passing nil to Transaction.SetWebRequestHTTP // allows you to do just this. func ExampleTransaction_SetWebRequestHTTP_nil() { app := getApp() txn := app.StartTransaction("My-Transaction") // Mark transaction as a web transaction, but do not record attributes // based on an inbound request or read distributed tracing headers. txn.SetWebRequestHTTP(nil) } // This example (modified from the WrapHandle instrumentation) demonstrates how // you can replace an http.ResponseWriter in order to capture response headers // and notice errors based on status code. // // Note that this is just an example and that WrapHandle and WrapHandleFunc // perform this instrumentation for you. func ExampleTransaction_SetWebResponse() { app := getApp() handler := http.FileServer(http.Dir("/tmp")) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Begin a Transaction. txn := app.StartTransaction("index") defer txn.End() // Set the transaction as a web request, gather attributes based on the // request, and read incoming distributed trace headers. txn.SetWebRequestHTTP(r) // Prepare to capture attributes, errors, and headers from the // response. w = txn.SetWebResponse(w) // Add the Transaction to the http.Request's Context. r = newrelic.RequestWithTransactionContext(r, txn) // The http.ResponseWriter passed to ServeHTTP has been replaced with // the new instrumented http.ResponseWriter. handler.ServeHTTP(w, r) }) } // The order in which the ConfigOptions are added plays an important role when // using ConfigFromEnvironment. func ExampleConfigFromEnvironment() { os.Setenv("NEW_RELIC_ENABLED", "true") // Application is disabled. Enabled is first set to true from // ConfigFromEnvironment then set to false from ConfigEnabled. _, _ = newrelic.NewApplication( newrelic.ConfigFromEnvironment(), newrelic.ConfigEnabled(false), ) // Application is enabled. Enabled is first set to false from // ConfigEnabled then set to true from ConfigFromEnvironment. _, _ = newrelic.NewApplication( newrelic.ConfigEnabled(false), newrelic.ConfigFromEnvironment(), ) } func ExampleNewApplication_configOptionOrder() { // In this case, the Application will be disabled because the disabling // ConfigOption is last. _, _ = newrelic.NewApplication( newrelic.ConfigEnabled(true), newrelic.ConfigEnabled(false), ) } // While many ConfigOptions are provided for you, it is also possible to create // your own. This is necessary if you have complex configuration needs. func ExampleConfigOption_custom() { _, _ = newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), func(cfg *newrelic.Config) { // Set specific Config fields inside a custom ConfigOption. cfg.Attributes.Enabled = false cfg.HighSecurity = true }, ) } // Setting the Config.Error field will cause the NewApplication function to // return an error. func ExampleConfigOption_errors() { myError := errors.New("oops") _, err := newrelic.NewApplication( newrelic.ConfigAppName("Example App"), newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"), func(cfg *newrelic.Config) { cfg.Error = myError }, ) fmt.Printf("%t", err == myError) // Output: true } func ExampleTransaction_StartSegmentNow() { txn := currentTransaction() seg := &newrelic.MessageProducerSegment{ // The value returned from Transaction.StartSegmentNow is used for the // StartTime field on any segment. StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: newrelic.MessageExchange, DestinationName: "myExchange", } // add message to queue here seg.End() } // Passing a new Transaction reference directly to another goroutine. func ExampleTransaction_NewGoroutine() { go func(txn *newrelic.Transaction) { defer txn.StartSegment("async").End() // Do some work time.Sleep(100 * time.Millisecond) }(txn.NewGoroutine()) } // Passing a new Transaction reference on a channel to another goroutine. func ExampleTransaction_NewGoroutine_channel() { ch := make(chan *newrelic.Transaction) go func() { txn := <-ch defer txn.StartSegment("async").End() // Do some work time.Sleep(100 * time.Millisecond) }() ch <- txn.NewGoroutine() } // Sometimes it is not possible to call txn.NewGoroutine() before the goroutine // has started. In this case, it is okay to call the method from inside the // newly started goroutine. func ExampleTransaction_NewGoroutine_insideGoroutines() { // async will always be called using `go async(ctx)` async := func(ctx context.Context) { txn := newrelic.FromContext(ctx) txn = txn.NewGoroutine() defer txn.StartSegment("async").End() // Do some work time.Sleep(100 * time.Millisecond) } ctx := newrelic.NewContext(context.Background(), currentTransaction()) go async(ctx) } go-agent-3.42.0/v3/newrelic/expect_implementation.go000066400000000000000000000501461510742411500223730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" "fmt" "reflect" "time" "github.com/newrelic/go-agent/v3/internal" ) func validateStringField(v internal.Validator, fieldName, expect, actual string) { // If an expected value is not set, we assume the user does not want to validate it if expect == "" { return } if expect != actual { v.Error(fieldName, "incorrect: Expected:", expect, " Got:", actual) } } type addValidatorField struct { field interface{} original internal.Validator } func (a addValidatorField) Error(fields ...interface{}) { fields = append([]interface{}{a.field}, fields...) a.original.Error(fields...) } // extendValidator is used to add more context to a validator. func extendValidator(v internal.Validator, field interface{}) internal.Validator { return addValidatorField{ field: field, original: v, } } // expectTxnMetrics tests that the app contains metrics for a transaction. func expectTxnMetrics(t internal.Validator, mt *metricTable, want internal.WantTxn) { var metrics []internal.WantMetric var scope string var allWebOther string if want.IsWeb { scope = "WebTransaction/Go/" + want.Name allWebOther = "allWeb" metrics = []internal.WantMetric{ {Name: "WebTransaction/Go/" + want.Name, Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/" + want.Name, Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/" + want.Name, Scope: "", Forced: false, Data: nil}, } if want.UnknownCaller { metrics = append(metrics, internal.WantMetric{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, ) metrics = append(metrics, internal.WantMetric{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, ) } if want.ErrorByCaller { metrics = append(metrics, internal.WantMetric{Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, ) metrics = append(metrics, internal.WantMetric{Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, ) } } else { scope = "OtherTransaction/Go/" + want.Name allWebOther = "allOther" metrics = []internal.WantMetric{ {Name: "OtherTransaction/Go/" + want.Name, Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/" + want.Name, Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, } if want.UnknownCaller { metrics = append(metrics, internal.WantMetric{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, ) metrics = append(metrics, internal.WantMetric{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, ) } if want.ErrorByCaller { metrics = append(metrics, internal.WantMetric{Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, ) metrics = append(metrics, internal.WantMetric{Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, ) } } if want.NumErrors > 0 { data := []float64{float64(want.NumErrors), 0, 0, 0, 0, 0} metrics = append(metrics, []internal.WantMetric{ {Name: "Errors/all", Scope: "", Forced: true, Data: data}, {Name: "Errors/" + allWebOther, Scope: "", Forced: true, Data: data}, {Name: "Errors/" + scope, Scope: "", Forced: true, Data: data}, }...) } expectMetrics(t, mt, metrics) } func expectMetricField(t internal.Validator, id metricID, expect, want float64, fieldName string) { if expect != want { t.Error("incorrect value for metric", fieldName, id, "expect:", expect, "want: ", want) } } // expectMetricsPresent allows testing of metrics without requiring an exact match func expectMetricsPresent(t internal.Validator, mt *metricTable, expect []internal.WantMetric) { expectMetricsInternal(t, mt, expect, false) } // expectMetrics allows testing of metrics. It passes if mt exactly matches expect. func expectMetrics(t internal.Validator, mt *metricTable, expect []internal.WantMetric) { expectMetricsInternal(t, mt, expect, true) } func expectMetricsInternal(t internal.Validator, mt *metricTable, expect []internal.WantMetric, exactMatch bool) { if exactMatch { if len(mt.metrics) != len(expect) { t.Error("incorrect number of metrics stored, expected:", len(expect), "got:", len(mt.metrics)) } } expectedIds := make(map[metricID]struct{}) for _, e := range expect { id := metricID{Name: e.Name, Scope: e.Scope} expectedIds[id] = struct{}{} m := mt.metrics[id] if nil == m { t.Error("expected metric not found", id) continue } if b, ok := e.Forced.(bool); ok { if b != (forced == m.forced) { t.Error("metric forced incorrect", b, m.forced, id) } } if nil != e.Data { expectMetricField(t, id, e.Data[0], m.data.countSatisfied, "countSatisfied") if len(e.Data) > 1 { expectMetricField(t, id, e.Data[1], m.data.totalTolerated, "totalTolerated") expectMetricField(t, id, e.Data[2], m.data.exclusiveFailed, "exclusiveFailed") expectMetricField(t, id, e.Data[3], m.data.min, "min") expectMetricField(t, id, e.Data[4], m.data.max, "max") expectMetricField(t, id, e.Data[5], m.data.sumSquares, "sumSquares") } } } if exactMatch { for id := range mt.metrics { if _, ok := expectedIds[id]; !ok { t.Error("expected metrics does not contain", id.Name, id.Scope) } } } } func expectAttributes(v internal.Validator, exists map[string]interface{}, expect map[string]interface{}) { // TODO: This params comparison can be made smarter: Alert differences // based on sub/super set behavior. if len(exists) != len(expect) { v.Error("attributes length difference", len(exists), len(expect)) } for key, expectVal := range expect { actualVal, ok := exists[key] if !ok { v.Error("expected attribute not found: ", key) continue } if expectVal == internal.MatchAnything || expectVal == "*" { continue } actualString := fmt.Sprint(actualVal) expectString := fmt.Sprint(expectVal) switch expectVal.(type) { case float64: // json.Number type objects need to be converted into float64 strings // when compared against a float64 or the comparison will fail due to // the number formatting being different if number, ok := actualVal.(json.Number); ok { numString, _ := number.Float64() actualString = fmt.Sprint(numString) } } if expectString != actualString { v.Error(fmt.Sprintf("Values of key \"%s\" do not match; Expect: %s Actual: %s", key, expectString, actualString)) } } for key, val := range exists { _, ok := expect[key] if !ok { v.Error("unexpected attribute present: ", key, val) continue } } } // expectCustomEvents allows testing of custom events. It passes if cs exactly matches expect. func expectCustomEvents(v internal.Validator, cs *customEvents, expect []internal.WantEvent) { expectEvents(v, cs.analyticsEvents, expect, nil) } func expectLogEvents(v internal.Validator, events *logEvents, expect []internal.WantLog) { if len(events.logs) != len(expect) { v.Error("actual number of events does not match what is expected", len(events.logs), len(expect)) return } for i, e := range expect { event := events.logs[i] expectLogEvent(v, event, e) } } func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog) { if actual.message != want.Message && want.Message != internal.MatchAnyString { v.Error(fmt.Sprintf("unexpected log message: got %s, want %s", actual.message, want.Message)) return } if actual.severity != want.Severity && want.Severity != internal.MatchAnyString { v.Error(fmt.Sprintf("unexpected log severity: got %s, want %s", actual.severity, want.Severity)) return } if actual.traceID != want.TraceID && want.TraceID != internal.MatchAnyString { v.Error(fmt.Sprintf("unexpected log trace id: got %s, want %s", actual.traceID, want.TraceID)) return } if actual.spanID != want.SpanID && want.SpanID != internal.MatchAnyString { v.Error(fmt.Sprintf("unexpected log span id: got %s, want %s", actual.spanID, want.SpanID)) return } if actual.timestamp != want.Timestamp && want.Timestamp != internal.MatchAnyUnixMilli { v.Error(fmt.Sprintf("unexpected log timestamp: got %d, want %d", actual.timestamp, want.Timestamp)) return } if actual.attributes != nil && want.Attributes != nil { if len(actual.attributes) != len(want.Attributes) { skippedAttributes := []string{} for k := range actual.attributes { if _, ok := want.Attributes[k]; !ok { skippedAttributes = append(skippedAttributes, fmt.Sprintf("an expected attribute is missing: {\"%s\":%v}", k, actual.attributes[k])) } } for k := range want.Attributes { if _, ok := actual.attributes[k]; !ok { skippedAttributes = append(skippedAttributes, fmt.Sprintf("unexpected attribute: {\"%s\":%v}", k, want.Attributes[k])) } } v.Error(fmt.Sprintf("unexpected number of log attributes: got %d, want %d; %s", len(actual.attributes), len(want.Attributes), skippedAttributes)) return } else { for k, wantVal := range want.Attributes { actualVal := actual.attributes[k] ok := reflect.DeepEqual(wantVal, actualVal) if !ok { v.Error(fmt.Sprintf("unexpected log attribute for key \"%s\": got value: %+v, type: %T; want value: %+v, type: %T", k, actualVal, actualVal, wantVal, wantVal)) return } } } } } func expectEvent(v internal.Validator, e json.Marshaler, expect internal.WantEvent) { js, err := e.MarshalJSON() if nil != err { v.Error("unable to marshal event", err) return } // Because we are unmarshaling into a generic struct without types // JSON numbers will be set to the float64 type by default, causing // errors when comparing to the expected integer timestamp value. decoder := json.NewDecoder(bytes.NewReader(js)) decoder.UseNumber() var event []map[string]interface{} err = decoder.Decode(&event) if nil != err { v.Error("unable to parse event json", err) return } // avoid nil pointer errors or index out of bounds errors if event == nil || len(event) == 0 { v.Error("Event can not be nil or empty") return } intrinsics := event[0] userAttributes := event[1] agentAttributes := event[2] if nil != expect.Intrinsics { expectAttributes(v, intrinsics, expect.Intrinsics) } if nil != expect.UserAttributes { expectAttributes(v, userAttributes, expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, agentAttributes, expect.AgentAttributes) } } func expectEvents(v internal.Validator, events *analyticsEvents, expect []internal.WantEvent, extraAttributes map[string]interface{}) { if len(events.events) != len(expect) { v.Error("number of events does not match", len(events.events), len(expect)) return } for i, e := range expect { event, ok := events.events[i].jsonWriter.(json.Marshaler) if !ok { v.Error("event does not implement json.Marshaler") continue } if nil != e.Intrinsics { e.Intrinsics = mergeAttributes(extraAttributes, e.Intrinsics) } expectEvent(v, event, e) } } // Second attributes have priority. func mergeAttributes(a1, a2 map[string]interface{}) map[string]interface{} { a := make(map[string]interface{}) for k, v := range a1 { a[k] = v } for k, v := range a2 { a[k] = v } return a } // expectErrorEvents allows testing of error events. It passes if events exactly matches expect. func expectErrorEvents(v internal.Validator, events *errorEvents, expect []internal.WantEvent) { expectEvents(v, events.analyticsEvents, expect, map[string]interface{}{ // The following intrinsics should always be present in // error events: "type": "TransactionError", "timestamp": internal.MatchAnything, "duration": internal.MatchAnything, }) } // expectSpanEvents allows testing of span events. It passes if events exactly matches expect. func expectSpanEvents(v internal.Validator, events *spanEvents, expect []internal.WantEvent) { extraAttrs := map[string]interface{}{ // The following intrinsics should always be present in // span events: "type": "Span", "timestamp": internal.MatchAnything, "duration": internal.MatchAnything, "traceId": internal.MatchAnything, "guid": internal.MatchAnything, "transactionId": internal.MatchAnything, // All span events are currently sampled. "sampled": true, "priority": internal.MatchAnything, } expectEvents(v, events.analyticsEvents, expect, extraAttrs) expectObserverEvents(v, events.analyticsEvents, expect, extraAttrs) } // expectTxnEvents allows testing of txn events. func expectTxnEvents(v internal.Validator, events *txnEvents, expect []internal.WantEvent) { expectEvents(v, events.analyticsEvents, expect, map[string]interface{}{ // The following intrinsics should always be present in // txn events: "type": "Transaction", "timestamp": internal.MatchAnything, "duration": internal.MatchAnything, "totalTime": internal.MatchAnything, "error": internal.MatchAnything, }) } func expectError(v internal.Validator, err *tracedError, expect internal.WantError) { validateStringField(v, "txnName", expect.TxnName, err.FinalName) validateStringField(v, "klass", expect.Klass, err.Klass) validateStringField(v, "msg", expect.Msg, err.Msg) js, errr := err.MarshalJSON() if nil != errr { v.Error("unable to marshal error json", errr) return } var unmarshalled []interface{} errr = json.Unmarshal(js, &unmarshalled) if nil != errr { v.Error("unable to unmarshal error json", errr) return } attributes := unmarshalled[4].(map[string]interface{}) agentAttributes := attributes["agentAttributes"].(map[string]interface{}) userAttributes := attributes["userAttributes"].(map[string]interface{}) if nil != expect.UserAttributes { expectAttributes(v, userAttributes, expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, agentAttributes, expect.AgentAttributes) } if stack := attributes["stack_trace"]; nil == stack { v.Error("missing error stack trace") } } // expectErrors allows testing of errors. func expectErrors(v internal.Validator, errors harvestErrors, expect []internal.WantError) { if len(errors) != len(expect) { v.Error("number of errors mismatch", len(errors), len(expect)) return } for i, e := range expect { expectError(v, errors[i], e) } } func countSegments(node []interface{}) int { count := 1 children := node[4].([]interface{}) for _, c := range children { node := c.([]interface{}) count += countSegments(node) } return count } func expectTraceSegment(v internal.Validator, nodeObj interface{}, expect internal.WantTraceSegment) { node := nodeObj.([]interface{}) start := int(node[0].(float64)) stop := int(node[1].(float64)) name := node[2].(string) attributes := node[3].(map[string]interface{}) children := node[4].([]interface{}) validateStringField(v, "segmentName", expect.SegmentName, name) if nil != expect.RelativeStartMillis { expectStart, ok := expect.RelativeStartMillis.(int) if !ok { v.Error("invalid expect.RelativeStartMillis", expect.RelativeStartMillis) } else if expectStart != start { v.Error("segmentStartTime", expect.SegmentName, start, expectStart) } } if nil != expect.RelativeStopMillis { expectStop, ok := expect.RelativeStopMillis.(int) if !ok { v.Error("invalid expect.RelativeStopMillis", expect.RelativeStopMillis) } else if expectStop != stop { v.Error("segmentStopTime", expect.SegmentName, stop, expectStop) } } if nil != expect.Attributes { expectAttributes(v, attributes, expect.Attributes) } if len(children) != len(expect.Children) { v.Error("segmentChildrenCount", expect.SegmentName, len(children), len(expect.Children)) } else { for idx, child := range children { expectTraceSegment(v, child, expect.Children[idx]) } } } func expectTxnTrace(v internal.Validator, got interface{}, expect internal.WantTxnTrace) { unmarshalled := got.([]interface{}) duration := unmarshalled[1].(float64) name := unmarshalled[2].(string) var arrayURL string if nil != unmarshalled[3] { arrayURL = unmarshalled[3].(string) } traceData := unmarshalled[4].([]interface{}) rootNode := traceData[3].([]interface{}) attributes := traceData[4].(map[string]interface{}) userAttributes := attributes["userAttributes"].(map[string]interface{}) agentAttributes := attributes["agentAttributes"].(map[string]interface{}) intrinsics := attributes["intrinsics"].(map[string]interface{}) validateStringField(v, "metric name", expect.MetricName, name) if d := expect.DurationMillis; nil != d && *d != duration { v.Error("incorrect trace duration millis", *d, duration) } if nil != expect.UserAttributes { expectAttributes(v, userAttributes, expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, agentAttributes, expect.AgentAttributes) expectURL, _ := expect.AgentAttributes["request.uri"].(string) if "" != expectURL { validateStringField(v, "request url in array", expectURL, arrayURL) } } if nil != expect.Intrinsics { expectAttributes(v, intrinsics, expect.Intrinsics) } if expect.Root.SegmentName != "" { expectTraceSegment(v, rootNode, expect.Root) } else { numSegments := countSegments(rootNode) // The expectation segment count does not include the two root nodes. numSegments -= 2 if expect.NumSegments != numSegments { v.Error("wrong number of segments", expect.NumSegments, numSegments) } } } // expectTxnTraces allows testing of transaction traces. func expectTxnTraces(v internal.Validator, traces *harvestTraces, want []internal.WantTxnTrace) { if len(want) != traces.Len() { v.Error("number of traces do not match", len(want), traces.Len()) return } if len(want) == 0 { return } js, err := traces.Data("agentRunID", time.Now()) if nil != err { v.Error("error creasing harvest traces data", err) return } var unmarshalled []interface{} err = json.Unmarshal(js, &unmarshalled) if nil != err { v.Error("unable to unmarshal error json", err) return } if "agentRunID" != unmarshalled[0].(string) { v.Error("traces agent run id wrong", unmarshalled[0]) return } gotTraces := unmarshalled[1].([]interface{}) if len(gotTraces) != len(want) { v.Error("number of traces in json does not match", len(gotTraces), len(want)) return } for i, expected := range want { expectTxnTrace(v, gotTraces[i], expected) } } func expectSlowQuery(t internal.Validator, slowQuery *slowQuery, want internal.WantSlowQuery) { if slowQuery.Count != want.Count { t.Error("wrong Count field", slowQuery.Count, want.Count) } uri, _ := slowQuery.txnEvent.Attrs.GetAgentValue(AttributeRequestURI, destTxnTrace) validateStringField(t, "MetricName", want.MetricName, slowQuery.DatastoreMetric) validateStringField(t, "Query", want.Query, slowQuery.ParameterizedQuery) validateStringField(t, "TxnEvent.FinalName", want.TxnName, slowQuery.txnEvent.FinalName) validateStringField(t, "request.uri", want.TxnURL, uri) validateStringField(t, "DatabaseName", want.DatabaseName, slowQuery.DatabaseName) validateStringField(t, "Host", want.Host, slowQuery.Host) validateStringField(t, "PortPathOrID", want.PortPathOrID, slowQuery.PortPathOrID) expectAttributes(t, map[string]interface{}(slowQuery.QueryParameters), want.Params) } // expectSlowQueries allows testing of slow queries. func expectSlowQueries(t internal.Validator, slowQueries *slowQueries, want []internal.WantSlowQuery) { if len(want) != len(slowQueries.priorityQueue) { t.Error("wrong number of slow queries", "expected", len(want), "got", len(slowQueries.priorityQueue)) return } for _, s := range want { idx, ok := slowQueries.lookup[s.Query] if !ok { t.Error("unable to find slow query", s.Query) continue } expectSlowQuery(t, slowQueries.priorityQueue[idx], s) } } go-agent-3.42.0/v3/newrelic/harvest.go000066400000000000000000000260651510742411500174550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "time" "github.com/newrelic/go-agent/v3/internal" ) // harvestable is something that can be merged into a harvest. type harvestable interface { MergeIntoHarvest(h *harvest) } // harvestTypes is a bit set used to indicate which data types are ready to be // reported. type harvestTypes uint const ( harvestMetricsTraces harvestTypes = 1 << iota harvestSpanEvents harvestCustomEvents harvestLogEvents harvestTxnEvents harvestErrorEvents ) const ( // harvestTypesEvents includes all Event types harvestTypesEvents = harvestSpanEvents | harvestCustomEvents | harvestTxnEvents | harvestErrorEvents | harvestLogEvents // harvestTypesAll includes all harvest types harvestTypesAll = harvestMetricsTraces | harvestTypesEvents ) type harvestTimer struct { periods map[harvestTypes]time.Duration lastHarvest map[harvestTypes]time.Time } func newHarvestTimer(now time.Time, periods map[harvestTypes]time.Duration) *harvestTimer { lastHarvest := make(map[harvestTypes]time.Time, len(periods)) for tp := range periods { lastHarvest[tp] = now } return &harvestTimer{periods: periods, lastHarvest: lastHarvest} } func (timer *harvestTimer) ready(now time.Time) (ready harvestTypes) { for tp, period := range timer.periods { if deadline := timer.lastHarvest[tp].Add(period); now.After(deadline) { timer.lastHarvest[tp] = deadline ready |= tp } } return } // harvest contains collected data. type harvest struct { timer *harvestTimer Metrics *metricTable ErrorTraces harvestErrors TxnTraces *harvestTraces SlowSQLs *slowQueries SpanEvents *spanEvents CustomEvents *customEvents LogEvents *logEvents TxnEvents *txnEvents ErrorEvents *errorEvents } const ( // txnEventPayloadlimit is the maximum number of events that should be // sent up in one post. txnEventPayloadlimit = 5000 ) // Ready returns a new harvest which contains the data types ready for harvest, // or nil if no data is ready for harvest. func (h *harvest) Ready(now time.Time) *harvest { ready := &harvest{} types := h.timer.ready(now) if 0 == types { return nil } if 0 != types&harvestCustomEvents { h.Metrics.addCount(customEventsSeen, h.CustomEvents.NumSeen(), forced) h.Metrics.addCount(customEventsSent, h.CustomEvents.NumSaved(), forced) ready.CustomEvents = h.CustomEvents h.CustomEvents = newCustomEvents(h.CustomEvents.capacity()) } if 0 != types&harvestLogEvents { h.LogEvents.RecordLoggingMetrics(h.Metrics) ready.LogEvents = h.LogEvents h.LogEvents = newLogEvents(h.LogEvents.commonAttributes, h.LogEvents.config) } if 0 != types&harvestTxnEvents { h.Metrics.addCount(txnEventsSeen, h.TxnEvents.NumSeen(), forced) h.Metrics.addCount(txnEventsSent, h.TxnEvents.NumSaved(), forced) ready.TxnEvents = h.TxnEvents h.TxnEvents = newTxnEvents(h.TxnEvents.capacity()) } if 0 != types&harvestErrorEvents { h.Metrics.addCount(errorEventsSeen, h.ErrorEvents.NumSeen(), forced) h.Metrics.addCount(errorEventsSent, h.ErrorEvents.NumSaved(), forced) ready.ErrorEvents = h.ErrorEvents h.ErrorEvents = newErrorEvents(h.ErrorEvents.capacity()) } if 0 != types&harvestSpanEvents { h.Metrics.addCount(spanEventsSeen, h.SpanEvents.NumSeen(), forced) h.Metrics.addCount(spanEventsSent, h.SpanEvents.NumSaved(), forced) ready.SpanEvents = h.SpanEvents h.SpanEvents = newSpanEvents(h.SpanEvents.capacity()) } // NOTE! Metrics must happen after the event harvest conditionals to // ensure that the metrics contain the event supportability metrics. if 0 != types&harvestMetricsTraces { ready.Metrics = h.Metrics ready.ErrorTraces = h.ErrorTraces ready.SlowSQLs = h.SlowSQLs ready.TxnTraces = h.TxnTraces h.Metrics = newMetricTable(maxMetrics, now) h.ErrorTraces = newHarvestErrors(maxHarvestErrors) h.SlowSQLs = newSlowQueries(maxHarvestSlowSQLs) h.TxnTraces = newHarvestTraces() } return ready } // Payloads returns a slice of payload creators. func (h *harvest) Payloads(splitLargeTxnEvents bool) (ps []payloadCreator) { if nil == h { return } if nil != h.CustomEvents { ps = append(ps, h.CustomEvents) } if nil != h.LogEvents { ps = append(ps, h.LogEvents) } if nil != h.ErrorEvents { ps = append(ps, h.ErrorEvents) } if nil != h.SpanEvents { ps = append(ps, h.SpanEvents) } if nil != h.Metrics { ps = append(ps, h.Metrics) } if nil != h.ErrorTraces { ps = append(ps, h.ErrorTraces) } if nil != h.TxnTraces { ps = append(ps, h.TxnTraces) } if nil != h.SlowSQLs { ps = append(ps, h.SlowSQLs) } if nil != h.TxnEvents { if splitLargeTxnEvents { ps = append(ps, h.TxnEvents.payloads(txnEventPayloadlimit)...) } else { ps = append(ps, h.TxnEvents) } } return } type harvestConfig struct { ReportPeriods map[harvestTypes]time.Duration CommonAttributes commonAttributes LoggingConfig loggingConfig MaxSpanEvents int MaxCustomEvents int MaxErrorEvents int MaxTxnEvents int } // newHarvest returns a new Harvest. func newHarvest(now time.Time, configurer harvestConfig) *harvest { return &harvest{ timer: newHarvestTimer(now, configurer.ReportPeriods), Metrics: newMetricTable(maxMetrics, now), ErrorTraces: newHarvestErrors(maxHarvestErrors), TxnTraces: newHarvestTraces(), SlowSQLs: newSlowQueries(maxHarvestSlowSQLs), SpanEvents: newSpanEvents(configurer.MaxSpanEvents), CustomEvents: newCustomEvents(configurer.MaxCustomEvents), LogEvents: newLogEvents(configurer.CommonAttributes, configurer.LoggingConfig), TxnEvents: newTxnEvents(configurer.MaxTxnEvents), ErrorEvents: newErrorEvents(configurer.MaxErrorEvents), } } func createTrackUsageMetrics(metrics *metricTable) { for _, m := range internal.GetUsageSupportabilityMetrics() { metrics.addSingleCount(m, forced) } } func createTraceObserverMetrics(to traceObserver, metrics *metricTable) { if to == nil { return } for name, val := range to.dumpSupportabilityMetrics() { metrics.addCount(name, val, forced) } } func createAppLoggingSupportabilityMetrics(lc *loggingConfig, metrics *metricTable) { lc.connectMetrics(metrics) } // CreateFinalMetrics creates extra metrics at harvest time. func (h *harvest) CreateFinalMetrics(run *appRun, to traceObserver) { reply := run.Reply hc := run.harvestConfig if nil == h { return } // Metrics will be non-nil when harvesting metrics (regardless of // whether or not there are any metrics to send). if nil == h.Metrics { return } h.Metrics.addSingleCount(instanceReporting, forced) // Configurable event harvest supportability metrics: // https://source.datanerd.us/agents/agent-specs/blob/master/Connect-LEGACY.md#event-harvest-config period := reply.ConfigurablePeriod() h.Metrics.addDuration(supportReportPeriod, "", period, period, forced) h.Metrics.addValue(supportTxnEventLimit, "", float64(hc.MaxTxnEvents), forced) h.Metrics.addValue(supportCustomEventLimit, "", float64(hc.MaxCustomEvents), forced) h.Metrics.addValue(supportErrorEventLimit, "", float64(hc.MaxErrorEvents), forced) h.Metrics.addValue(supportSpanEventLimit, "", float64(hc.MaxSpanEvents), forced) h.Metrics.addValue(supportLogEventLimit, "", float64(hc.LoggingConfig.maxLogEvents), forced) createTraceObserverMetrics(to, h.Metrics) createTrackUsageMetrics(h.Metrics) createAppLoggingSupportabilityMetrics(&hc.LoggingConfig, h.Metrics) h.Metrics = h.Metrics.ApplyRules(reply.MetricRules) } // payloadCreator is a data type in the harvest. type payloadCreator interface { // In the event of a rpm request failure (hopefully simply an // intermittent collector issue) the payload may be merged into the next // time period's harvest. harvestable // Data prepares JSON in the format expected by the collector endpoint. // This method should return (nil, nil) if the payload is empty and no // rpm request is necessary. Data(agentRunID string, harvestStart time.Time) ([]byte, error) // EndpointMethod is used for the "method" query parameter when posting // the data. EndpointMethod() string } // createTxnMetrics creates metrics for a transaction. func createTxnMetrics(args *txnData, metrics *metricTable) { withoutFirstSegment := removeFirstSegment(args.FinalName) // Duration Metrics var durationRollup string var totalTimeRollup string if args.IsWeb { durationRollup = webRollup totalTimeRollup = totalTimeWeb metrics.addDuration(dispatcherMetric, "", args.Duration, 0, forced) } else { durationRollup = backgroundRollup totalTimeRollup = totalTimeBackground } metrics.addDuration(args.FinalName, "", args.Duration, 0, forced) metrics.addDuration(durationRollup, "", args.Duration, 0, forced) metrics.addDuration(totalTimeRollup, "", args.TotalTime, args.TotalTime, forced) metrics.addDuration(totalTimeRollup+"/"+withoutFirstSegment, "", args.TotalTime, args.TotalTime, unforced) // Better CAT Metrics if cat := args.BetterCAT; cat.Enabled { caller := callerUnknown if nil != cat.Inbound && cat.Inbound.HasNewRelicTraceInfo { caller.Type = cat.Inbound.Type caller.App = cat.Inbound.App caller.Account = cat.Inbound.Account } if cat.TransportType != "" { caller.TransportType = cat.TransportType } m := durationByCallerMetric(caller) metrics.addDuration(m.all, "", args.Duration, args.Duration, unforced) metrics.addDuration(m.webOrOther(args.IsWeb), "", args.Duration, args.Duration, unforced) // Transport Duration Metric if nil != cat.Inbound && cat.Inbound.HasNewRelicTraceInfo { d := cat.Inbound.TransportDuration m = transportDurationMetric(caller) metrics.addDuration(m.all, "", d, d, unforced) metrics.addDuration(m.webOrOther(args.IsWeb), "", d, d, unforced) } // CAT Error Metrics if args.HasErrors() { m = errorsByCallerMetric(caller) metrics.addSingleCount(m.all, unforced) metrics.addSingleCount(m.webOrOther(args.IsWeb), unforced) } args.DistributedTracingSupport.createMetrics(metrics) } // Apdex Metrics if args.Zone != apdexNone { metrics.addApdex(apdexRollup, "", args.ApdexThreshold, args.Zone, forced) mname := apdexPrefix + withoutFirstSegment metrics.addApdex(mname, "", args.ApdexThreshold, args.Zone, unforced) } // Error Metrics if args.NoticeErrors() { metrics.addSingleCount(errorsRollupMetric.all, forced) metrics.addSingleCount(errorsRollupMetric.webOrOther(args.IsWeb), forced) metrics.addSingleCount(errorsPrefix+args.FinalName, forced) } if args.HasExpectedErrors() { metrics.addSingleCount(expectedErrorsRollupMetric.all, forced) } // Queueing Metrics if args.Queuing > 0 { metrics.addDuration(queueMetric, "", args.Queuing, args.Queuing, forced) } } var ( // This should only be used by harvests in cases where a connect response is unavailable dfltHarvestCfgr = harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{harvestTypesAll: fixedHarvestPeriod}, MaxTxnEvents: internal.MaxTxnEvents, MaxSpanEvents: internal.MaxSpanEvents, MaxCustomEvents: internal.MaxCustomEvents, MaxErrorEvents: internal.MaxErrorEvents, LoggingConfig: loggingConfig{ true, false, true, false, internal.MaxLogEvents, nil, nil, nil, }, } ) go-agent-3.42.0/v3/newrelic/harvest_test.go000066400000000000000000001203151510742411500205050ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" ) var ( // This is for testing only testHarvestCfgr = generateTestHarvestConfig() ) func generateTestHarvestConfig() harvestConfig { cfg := dfltHarvestCfgr // Enable logging features for testing (not enabled by default) loggingCfg := loggingConfigEnabled(internal.MaxLogEvents) cfg.LoggingConfig = loggingCfg return cfg } func TestHarvestTimerAllFixed(t *testing.T) { now := time.Now() harvest := newHarvest(now, testHarvestCfgr) timer := harvest.timer for _, tc := range []struct { Elapsed time.Duration Expect harvestTypes }{ {60 * time.Second, 0}, {61 * time.Second, harvestTypesAll}, {62 * time.Second, 0}, {120 * time.Second, 0}, {121 * time.Second, harvestTypesAll}, {122 * time.Second, 0}, } { if ready := timer.ready(now.Add(tc.Elapsed)); ready != tc.Expect { t.Error(tc.Elapsed, ready, tc.Expect) } } } func TestHarvestTimerAllConfigurable(t *testing.T) { now := time.Now() harvest := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ harvestMetricsTraces: fixedHarvestPeriod, harvestTypesEvents: time.Second * 30, }, MaxTxnEvents: 1, MaxCustomEvents: 2, MaxSpanEvents: 3, MaxErrorEvents: 4, }) timer := harvest.timer for _, tc := range []struct { Elapsed time.Duration Expect harvestTypes }{ {30 * time.Second, 0}, {31 * time.Second, harvestTypesEvents}, {32 * time.Second, 0}, {61 * time.Second, harvestTypesAll}, {62 * time.Second, 0}, {91 * time.Second, harvestTypesEvents}, {92 * time.Second, 0}, } { if ready := timer.ready(now.Add(tc.Elapsed)); ready != tc.Expect { t.Error(tc.Elapsed, ready, tc.Expect) } } } func TestCreateFinalMetrics(t *testing.T) { now := time.Now() // If the harvest or metrics is nil then CreateFinalMetrics should // not panic. var nilHarvest *harvest config := config{Config: defaultConfig()} run := newAppRun(config, internal.ConnectReplyDefaults()) run.harvestConfig = testHarvestCfgr nilHarvest.CreateFinalMetrics(run, nil) emptyHarvest := &harvest{} emptyHarvest.CreateFinalMetrics(run, nil) replyJSON := []byte(`{"return_value":{ "metric_name_rules":[{ "match_expression": "rename_me", "replacement": "been_renamed" }], "event_harvest_config":{ "report_period_ms": 2000, "harvest_limits": { "analytic_event_data": 22, "custom_event_data": 33, "error_event_data": 44, "span_event_data": 55, "log_event_data":66 } } }}`) reply, err := internal.UnmarshalConnectReply(replyJSON, internal.PreconnectReply{}) if err != nil { t.Fatal(err) } cfgr := harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ harvestMetricsTraces: fixedHarvestPeriod, harvestTypesEvents: time.Second * 2, }, MaxTxnEvents: 22, MaxCustomEvents: 33, MaxErrorEvents: 44, MaxSpanEvents: 55, LoggingConfig: loggingConfigEnabled(66), } h := newHarvest(now, cfgr) h.Metrics.addCount("rename_me", 1.0, unforced) run = newAppRun(config, reply) run.harvestConfig = cfgr h.CreateFinalMetrics(run, nil) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: instanceReporting, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "been_renamed", Scope: "", Forced: false, Data: []float64{1.0, 0, 0, 0, 0, 0}}, {Name: "Supportability/EventHarvest/ReportPeriod", Scope: "", Forced: true, Data: []float64{1, 2, 2, 2, 2, 2 * 2}}, {Name: "Supportability/EventHarvest/AnalyticEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 22, 22, 22, 22, 22 * 22}}, {Name: "Supportability/EventHarvest/CustomEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 33, 33, 33, 33, 33 * 33}}, {Name: "Supportability/EventHarvest/ErrorEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 44, 44, 44, 44, 44 * 44}}, {Name: "Supportability/EventHarvest/SpanEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 55, 55, 55, 55, 55 * 55}}, {Name: "Supportability/EventHarvest/LogEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 66, 66, 66, 66, 66 * 66}}, {Name: "Supportability/Go/Version/" + Version, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Go/Runtime/Version/" + goVersionSimple, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Go/gRPC/Version/" + grpcVersion, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Forwarding/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Metrics/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/LocalDecorating/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) // Test again without any metric rules or event_harvest_config. replyJSON = []byte(`{"return_value":{ }}`) reply, err = internal.UnmarshalConnectReply(replyJSON, internal.PreconnectReply{}) if err != nil { t.Fatal(err) } run = newAppRun(config, reply) run.harvestConfig = testHarvestCfgr h = newHarvest(now, testHarvestCfgr) h.Metrics.addCount("rename_me", 1.0, unforced) h.CreateFinalMetrics(run, nil) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: instanceReporting, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "rename_me", Scope: "", Forced: false, Data: []float64{1.0, 0, 0, 0, 0, 0}}, {Name: "Supportability/EventHarvest/ReportPeriod", Scope: "", Forced: true, Data: []float64{1, 60, 60, 60, 60, 60 * 60}}, {Name: "Supportability/EventHarvest/AnalyticEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000 * 10 * 1000}}, {Name: "Supportability/EventHarvest/CustomEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, internal.MaxCustomEvents, internal.MaxCustomEvents, internal.MaxCustomEvents, internal.MaxCustomEvents, internal.MaxCustomEvents * internal.MaxCustomEvents}}, {Name: "Supportability/EventHarvest/ErrorEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 100, 100, 100, 100, 100 * 100}}, {Name: "Supportability/EventHarvest/SpanEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, internal.MaxSpanEvents, internal.MaxSpanEvents, internal.MaxSpanEvents, internal.MaxSpanEvents, internal.MaxSpanEvents * internal.MaxSpanEvents}}, {Name: "Supportability/EventHarvest/LogEventData/HarvestLimit", Scope: "", Forced: true, Data: []float64{1, 10000, 10000, 10000, 10000, 10000 * 10000}}, {Name: "Supportability/Go/Version/" + Version, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Go/Runtime/Version/" + goVersionSimple, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Go/gRPC/Version/" + grpcVersion, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Forwarding/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/Metrics/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/Logging/LocalDecorating/Golang", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestCreateFinalMetricsTraceObserver(t *testing.T) { if !versionSupports8T { t.Skip("go version does not support 8T") } replyJSON := []byte(`{"return_value":{}}`) reply, err := internal.UnmarshalConnectReply(replyJSON, internal.PreconnectReply{}) if err != nil { t.Fatal(err) } run := newAppRun(config{Config: defaultConfig()}, reply) run.harvestConfig = testHarvestCfgr to, _ := newTraceObserver( internal.AgentRunID("runid"), nil, observerConfig{ log: logger.ShimLogger{}, }, ) h := newHarvest(now, testHarvestCfgr) h.CreateFinalMetrics(run, to) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: instanceReporting, Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/ReportPeriod", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/AnalyticEventData/HarvestLimit", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/CustomEventData/HarvestLimit", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/ErrorEventData/HarvestLimit", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/SpanEventData/HarvestLimit", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/EventHarvest/LogEventData/HarvestLimit", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Logging/Golang", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Logging/Forwarding/Golang", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Logging/Metrics/Golang", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Logging/LocalDecorating/Golang", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Go/Version/" + Version, Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Go/Runtime/Version/" + goVersionSimple, Scope: "", Forced: true, Data: nil}, {Name: "Supportability/Go/gRPC/Version/" + grpcVersion, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Supportability/InfiniteTracing/Span/Seen", Scope: "", Forced: true, Data: []float64{0, 0, 0, 0, 0, 0}}, {Name: "Supportability/InfiniteTracing/Span/Sent", Scope: "", Forced: true, Data: []float64{0, 0, 0, 0, 0, 0}}, }) } func TestEmptyPayloads(t *testing.T) { h := newHarvest(time.Now(), testHarvestCfgr) payloads := h.Payloads(true) if len(payloads) != 9 { t.Error(len(payloads)) } for _, p := range payloads { d, err := p.Data("agentRunID", time.Now()) if d != nil || err != nil { t.Error(d, err) } } } func TestPayloadsNilHarvest(t *testing.T) { var nilHarvest *harvest payloads := nilHarvest.Payloads(true) if len(payloads) != 0 { t.Error(len(payloads)) } } func TestPayloadsEmptyHarvest(t *testing.T) { h := &harvest{} payloads := h.Payloads(true) if len(payloads) != 0 { t.Error(len(payloads)) } } func TestHarvestNothingReady(t *testing.T) { now := time.Now() h := newHarvest(now, testHarvestCfgr) ready := h.Ready(now.Add(10 * time.Second)) if ready != nil { t.Error("harvest should be nil") } payloads := ready.Payloads(true) if len(payloads) != 0 { t.Error(payloads) } expectMetrics(t, h.Metrics, []internal.WantMetric{}) } func TestHarvestCustomEventsReady(t *testing.T) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestTxnEvents & harvestSpanEvents & harvestErrorEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestCustomEvents: time.Second * 5, }, MaxCustomEvents: 3, }) params := map[string]interface{}{"zip": 1} ce, _ := createCustomEvent("myEvent", params, time.Now()) h.CustomEvents.Add(ce) ready := h.Ready(now.Add(10 * time.Second)) payloads := ready.Payloads(true) if len(payloads) != 1 { t.Fatal(payloads) } p := payloads[0] if m := p.EndpointMethod(); m != "custom_event_data" { t.Error(m) } data, err := p.Data("agentRunID", now) if nil != err || nil == data { t.Error(err, data) } if h.CustomEvents.capacity() != 3 || h.CustomEvents.NumSaved() != 0 { t.Fatal("custom events not correctly reset") } expectCustomEvents(t, ready.CustomEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{"type": "myEvent", "timestamp": internal.MatchAnything}, UserAttributes: params, }}) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: customEventsSeen, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: customEventsSent, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestHarvestLogEventsReady(t *testing.T) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestTxnEvents & harvestSpanEvents & harvestLogEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestLogEvents: time.Second * 5, }, LoggingConfig: loggingConfigEnabled(3), }) logEvent := logEvent{ nil, 0.5, 123456, "INFO", "User 'xyz' logged in", "123456789ADF", "ADF09876565", } h.LogEvents.Add(&logEvent) ready := h.Ready(now.Add(10 * time.Second)) payloads := ready.Payloads(true) if len(payloads) == 0 { t.Fatal("no payloads generated") } else if len(payloads) > 1 { t.Fatalf("too many payloads: %d", len(payloads)) } p := payloads[0] if m := p.EndpointMethod(); m != "log_event_data" { t.Error(m) } data, err := p.Data("agentRunID", now) if nil != err || nil == data { t.Error(err, data) } if h.LogEvents.capacity() != 3 || h.LogEvents.NumSaved() != 0 { t.Fatal("log events not correctly reset") } sampleLogEvent := internal.WantLog{ Severity: logEvent.severity, Message: logEvent.message, SpanID: logEvent.spanID, TraceID: logEvent.traceID, Timestamp: logEvent.timestamp, } expectLogEvents(t, ready.LogEvents, []internal.WantLog{sampleLogEvent}) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: logsSeen, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: logsSeen + "/" + logEvent.severity, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: logsDropped, Scope: "", Forced: true, Data: []float64{0, 0, 0, 0, 0, 0}}, }) } func TestHarvestTxnEventsReady(t *testing.T) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestCustomEvents & harvestSpanEvents & harvestErrorEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestTxnEvents: time.Second * 5, }, MaxTxnEvents: 3, }) h.TxnEvents.AddTxnEvent(&txnEvent{ FinalName: "finalName", Start: time.Now(), Duration: 1 * time.Second, TotalTime: 2 * time.Second, }, 0) ready := h.Ready(now.Add(10 * time.Second)) payloads := ready.Payloads(true) if len(payloads) != 1 { t.Fatal(payloads) } p := payloads[0] if m := p.EndpointMethod(); m != "analytic_event_data" { t.Error(m) } data, err := p.Data("agentRunID", now) if nil != err || nil == data { t.Error(err, data) } if h.TxnEvents.capacity() != 3 || h.TxnEvents.NumSaved() != 0 { t.Fatal("txn events not correctly reset") } expectTxnEvents(t, ready.TxnEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "finalName", "totalTime": 2.0, }, }}) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: txnEventsSeen, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: txnEventsSent, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestHarvestErrorEventsReady(t *testing.T) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestCustomEvents & harvestSpanEvents & harvestTxnEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestErrorEvents: time.Second * 5, }, MaxErrorEvents: 3, }) h.ErrorEvents.Add(&errorEvent{ errorData: errorData{Klass: "klass", Msg: "msg", When: time.Now()}, txnEvent: txnEvent{FinalName: "finalName", Duration: 1 * time.Second, TxnID: "txn-guid-id"}, }, 0) ready := h.Ready(now.Add(10 * time.Second)) payloads := ready.Payloads(true) if len(payloads) != 1 { t.Fatal(payloads) } p := payloads[0] if m := p.EndpointMethod(); m != "error_event_data" { t.Error(m) } data, err := p.Data("agentRunID", now) if nil != err || nil == data { t.Error(err, data) } if h.ErrorEvents.capacity() != 3 || h.ErrorEvents.NumSaved() != 0 { t.Fatal("error events not correctly reset") } expectErrorEvents(t, ready.ErrorEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "klass", "error.message": "msg", "transactionName": "finalName", }, }}) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: errorEventsSeen, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: errorEventsSent, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestHarvestSpanEventsReady(t *testing.T) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestCustomEvents & harvestTxnEvents & harvestErrorEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestSpanEvents: time.Second * 5, }, MaxSpanEvents: 3, }) h.SpanEvents.addEventPopulated(&sampleSpanEvent) ready := h.Ready(now.Add(10 * time.Second)) payloads := ready.Payloads(true) if len(payloads) != 1 { t.Fatal(payloads) } p := payloads[0] if m := p.EndpointMethod(); m != "span_event_data" { t.Error(m) } data, err := p.Data("agentRunID", now) if nil != err || nil == data { t.Error(err, data) } if h.SpanEvents.capacity() != 3 || h.SpanEvents.NumSaved() != 0 { t.Fatal("span events not correctly reset") } expectSpanEvents(t, ready.SpanEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "Span", "name": "myName", "sampled": true, "priority": 0.5, "category": spanCategoryGeneric, "nr.entryPoint": true, "guid": "guid", "transactionId": "txn-id", "traceId": "trace-id", }, }}) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: spanEventsSeen, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: spanEventsSent, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestHarvestMetricsTracesReady(t *testing.T) { now := time.Now() h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ harvestMetricsTraces: fixedHarvestPeriod, harvestTypesEvents: time.Second * 65, }, MaxTxnEvents: 1, MaxCustomEvents: 1, MaxErrorEvents: 1, MaxSpanEvents: 1, LoggingConfig: loggingConfigEnabled(1), }) h.Metrics.addCount("zip", 1, forced) ers := newTxnErrors(10) ers.Add(errorData{When: time.Now(), Msg: "msg", Klass: "klass", Stack: getStackTrace()}) mergeTxnErrors(&h.ErrorTraces, ers, txnEvent{FinalName: "finalName", Attrs: nil}, nil) h.TxnTraces.Witness(harvestTrace{ txnEvent: txnEvent{ Start: time.Now(), Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", }, Trace: txnTrace{}, }) slows := newSlowQueries(maxTxnSlowQueries) slows.observeInstance(slowQueryInstance{ Duration: 2 * time.Second, DatastoreMetric: "Datastore/statement/MySQL/users/INSERT", ParameterizedQuery: "INSERT users", }) h.SlowSQLs.Merge(slows, txnEvent{FinalName: "finalName", Attrs: nil}) ready := h.Ready(now.Add(61 * time.Second)) payloads := ready.Payloads(true) if len(payloads) != 4 { t.Fatal(payloads) } expectMetrics(t, ready.Metrics, []internal.WantMetric{ {Name: "zip", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) expectMetrics(t, h.Metrics, []internal.WantMetric{}) expectErrors(t, ready.ErrorTraces, []internal.WantError{{ TxnName: "finalName", Msg: "msg", Klass: "klass", GUID: "error-guid-id", }}) expectErrors(t, h.ErrorTraces, []internal.WantError{}) expectSlowQueries(t, ready.SlowSQLs, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT users", TxnName: "finalName", }}) expectSlowQueries(t, h.SlowSQLs, []internal.WantSlowQuery{}) expectTxnTraces(t, ready.TxnTraces, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", }}) expectTxnTraces(t, h.TxnTraces, []internal.WantTxnTrace{}) } func TestMergeFailedHarvest(t *testing.T) { start1 := time.Now() start2 := start1.Add(1 * time.Minute) h := newHarvest(start1, testHarvestCfgr) h.Metrics.addCount("zip", 1, forced) h.TxnEvents.AddTxnEvent(&txnEvent{ FinalName: "finalName", Start: time.Now(), Duration: 1 * time.Second, TotalTime: 2 * time.Second, }, 0) logEvent := logEvent{ nil, 0.5, 123456, "INFO", "User 'xyz' logged in", "123456789ADF", "ADF09876565", } h.LogEvents.Add(&logEvent) customEventParams := map[string]interface{}{"zip": 1} ce, err := createCustomEvent("myEvent", customEventParams, time.Now()) if nil != err { t.Fatal(err) } h.CustomEvents.Add(ce) h.ErrorEvents.Add(&errorEvent{ errorData: errorData{ Klass: "klass", Msg: "msg", When: time.Now(), }, txnEvent: txnEvent{ FinalName: "finalName", Duration: 1 * time.Second, }, }, 0) ers := newTxnErrors(10) ers.Add(errorData{ When: time.Now(), Msg: "msg", Klass: "klass", Stack: getStackTrace(), }) mergeTxnErrors(&h.ErrorTraces, ers, txnEvent{ FinalName: "finalName", Attrs: nil, }, nil) h.SpanEvents.addEventPopulated(&sampleSpanEvent) if start1 != h.Metrics.metricPeriodStart { t.Error(h.Metrics.metricPeriodStart) } if 0 != h.Metrics.failedHarvests { t.Error(h.Metrics.failedHarvests) } if 0 != h.CustomEvents.analyticsEvents.failedHarvests { t.Error(h.CustomEvents.analyticsEvents.failedHarvests) } if 0 != h.LogEvents.failedHarvests { t.Error(h.LogEvents.failedHarvests) } if 0 != h.TxnEvents.analyticsEvents.failedHarvests { t.Error(h.TxnEvents.analyticsEvents.failedHarvests) } if 0 != h.ErrorEvents.analyticsEvents.failedHarvests { t.Error(h.ErrorEvents.analyticsEvents.failedHarvests) } if 0 != h.SpanEvents.analyticsEvents.failedHarvests { t.Error(h.SpanEvents.analyticsEvents.failedHarvests) } expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: "zip", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) expectCustomEvents(t, h.CustomEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "myEvent", "timestamp": internal.MatchAnything, }, UserAttributes: customEventParams, }}) expectLogEvents(t, h.LogEvents, []internal.WantLog{ { Severity: logEvent.severity, Message: logEvent.message, SpanID: logEvent.spanID, TraceID: logEvent.traceID, Timestamp: logEvent.timestamp, }, }) expectErrorEvents(t, h.ErrorEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "klass", "error.message": "msg", "transactionName": "finalName", }, }}) expectTxnEvents(t, h.TxnEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "finalName", "totalTime": 2.0, }, }}) expectSpanEvents(t, h.SpanEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "Span", "name": "myName", "sampled": true, "priority": 0.5, "category": spanCategoryGeneric, "nr.entryPoint": true, "guid": "guid", "transactionId": "txn-id", "traceId": "trace-id", }, }}) expectErrors(t, h.ErrorTraces, []internal.WantError{{ TxnName: "finalName", Msg: "msg", Klass: "klass", }}) nextHarvest := newHarvest(start2, testHarvestCfgr) if start2 != nextHarvest.Metrics.metricPeriodStart { t.Error(nextHarvest.Metrics.metricPeriodStart) } payloads := h.Payloads(true) for _, p := range payloads { p.MergeIntoHarvest(nextHarvest) } if start1 != nextHarvest.Metrics.metricPeriodStart { t.Error(nextHarvest.Metrics.metricPeriodStart) } if 1 != nextHarvest.Metrics.failedHarvests { t.Error(nextHarvest.Metrics.failedHarvests) } if 1 != nextHarvest.CustomEvents.analyticsEvents.failedHarvests { t.Error(nextHarvest.CustomEvents.analyticsEvents.failedHarvests) } if 1 != nextHarvest.LogEvents.failedHarvests { t.Error(nextHarvest.LogEvents.failedHarvests) } if 1 != nextHarvest.TxnEvents.analyticsEvents.failedHarvests { t.Error(nextHarvest.TxnEvents.analyticsEvents.failedHarvests) } if 1 != nextHarvest.ErrorEvents.analyticsEvents.failedHarvests { t.Error(nextHarvest.ErrorEvents.analyticsEvents.failedHarvests) } if 1 != nextHarvest.SpanEvents.analyticsEvents.failedHarvests { t.Error(nextHarvest.SpanEvents.analyticsEvents.failedHarvests) } expectMetrics(t, nextHarvest.Metrics, []internal.WantMetric{ {Name: "zip", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) expectCustomEvents(t, nextHarvest.CustomEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "myEvent", "timestamp": internal.MatchAnything, }, UserAttributes: customEventParams, }}) expectLogEvents(t, nextHarvest.LogEvents, []internal.WantLog{ { Severity: logEvent.severity, Message: logEvent.message, SpanID: logEvent.spanID, TraceID: logEvent.traceID, Timestamp: logEvent.timestamp, }, }) expectErrorEvents(t, nextHarvest.ErrorEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "klass", "error.message": "msg", "transactionName": "finalName", }, }}) expectTxnEvents(t, nextHarvest.TxnEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "finalName", "totalTime": 2.0, }, }}) expectSpanEvents(t, h.SpanEvents, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "Span", "name": "myName", "sampled": true, "priority": 0.5, "category": spanCategoryGeneric, "nr.entryPoint": true, "guid": "guid", "transactionId": "txn-id", "traceId": "trace-id", }, }}) expectErrors(t, nextHarvest.ErrorTraces, []internal.WantError{}) } func TestCreateTxnMetrics(t *testing.T) { txnErr := &errorData{} txnErrors := []*errorData{txnErr} webName := "WebTransaction/zip/zap" backgroundName := "OtherTransaction/zip/zap" args := &txnData{} args.noticeErrors = true args.Duration = 123 * time.Second args.TotalTime = 150 * time.Second args.ApdexThreshold = 2 * time.Second args.BetterCAT.Enabled = true args.FinalName = webName args.IsWeb = true args.Errors = txnErrors args.Zone = apdexTolerating metrics := newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: webName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: webRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: dispatcherMetric, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "WebTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/" + webName, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: apdexRollup, Scope: "", Forced: true, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "Apdex/zip/zap", Scope: "", Forced: false, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) args.FinalName = webName args.IsWeb = true args.Errors = nil args.noticeErrors = false args.Zone = apdexTolerating metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: webName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: webRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: dispatcherMetric, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "WebTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: apdexRollup, Scope: "", Forced: true, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "Apdex/zip/zap", Scope: "", Forced: false, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, }) args.FinalName = backgroundName args.IsWeb = false args.Errors = txnErrors args.noticeErrors = true args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/" + backgroundName, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) // Verify expected errors metrics args.FinalName = backgroundName args.IsWeb = false args.Errors = txnErrors args.noticeErrors = false args.expectedErrors = true args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "ErrorsExpected/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) args.FinalName = backgroundName args.IsWeb = false args.Errors = nil args.noticeErrors = false args.expectedErrors = false args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, }) } func TestHarvestSplitTxnEvents(t *testing.T) { now := time.Now() h := newHarvest(now, testHarvestCfgr) for i := 0; i < internal.MaxTxnEvents; i++ { h.TxnEvents.AddTxnEvent(&txnEvent{}, priority(float32(i))) } payloadsWithSplit := h.Payloads(true) payloadsWithoutSplit := h.Payloads(false) if len(payloadsWithSplit) != 10 { t.Error(len(payloadsWithSplit)) } if len(payloadsWithoutSplit) != 9 { t.Error(len(payloadsWithoutSplit)) } } func TestCreateTxnMetricsOldCAT(t *testing.T) { txnErr := &errorData{} txnErrors := []*errorData{txnErr} webName := "WebTransaction/zip/zap" backgroundName := "OtherTransaction/zip/zap" args := &txnData{} args.Duration = 123 * time.Second args.TotalTime = 150 * time.Second args.ApdexThreshold = 2 * time.Second // When BetterCAT is disabled, affirm that the caller metrics are not created. args.BetterCAT.Enabled = false args.FinalName = webName args.IsWeb = true args.Errors = txnErrors args.noticeErrors = true args.Zone = apdexTolerating metrics := newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: webName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: webRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: dispatcherMetric, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "WebTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/" + webName, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: apdexRollup, Scope: "", Forced: true, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "Apdex/zip/zap", Scope: "", Forced: false, Data: []float64{0, 1, 0, 2, 2, 0}}, }) args.FinalName = webName args.IsWeb = true args.Errors = nil args.noticeErrors = false args.Zone = apdexTolerating metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: webName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: webRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: dispatcherMetric, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "WebTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: apdexRollup, Scope: "", Forced: true, Data: []float64{0, 1, 0, 2, 2, 0}}, {Name: "Apdex/zip/zap", Scope: "", Forced: false, Data: []float64{0, 1, 0, 2, 2, 0}}, }) args.FinalName = backgroundName args.IsWeb = false args.Errors = txnErrors args.noticeErrors = true args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Errors/" + backgroundName, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) args.FinalName = backgroundName args.IsWeb = false args.Errors = nil args.noticeErrors = false args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, }) } func TestNewHarvestSetsDefaultValues(t *testing.T) { now := time.Now() h := newHarvest(now, testHarvestCfgr) if cp := h.TxnEvents.capacity(); cp != internal.MaxTxnEvents { t.Error("wrong txn event capacity", cp) } if cp := h.CustomEvents.capacity(); cp != internal.MaxCustomEvents { t.Error("wrong custom event capacity", cp) } if cp := h.LogEvents.capacity(); cp != internal.MaxLogEvents { t.Error("wrong log event capacity", cp) } if cp := h.ErrorEvents.capacity(); cp != internal.MaxErrorEvents { t.Error("wrong error event capacity", cp) } if cp := h.SpanEvents.capacity(); cp != internal.MaxSpanEvents { t.Error("wrong span event capacity", cp) } } func TestNewHarvestUsesConnectReply(t *testing.T) { now := time.Now() h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ harvestMetricsTraces: fixedHarvestPeriod, harvestTypesEvents: time.Second * 5, }, MaxTxnEvents: 1, MaxCustomEvents: 2, MaxErrorEvents: 3, MaxSpanEvents: 4, LoggingConfig: loggingConfigEnabled(5), }) if cp := h.TxnEvents.capacity(); cp != 1 { t.Error("wrong txn event capacity", cp) } if cp := h.CustomEvents.capacity(); cp != 2 { t.Error("wrong custom event capacity", cp) } if cp := h.ErrorEvents.capacity(); cp != 3 { t.Error("wrong error event capacity", cp) } if cp := h.SpanEvents.capacity(); cp != 4 { t.Error("wrong span event capacity", cp) } if cp := h.LogEvents.capacity(); cp != 5 { t.Error("wrong log event capacity", cp) } } func TestConfigurableHarvestZeroHarvestLimits(t *testing.T) { now := time.Now() h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ harvestMetricsTraces: fixedHarvestPeriod, harvestTypesEvents: time.Second * 5, }, MaxTxnEvents: 0, MaxCustomEvents: 0, MaxErrorEvents: 0, MaxSpanEvents: 0, LoggingConfig: loggingConfigEnabled(0), }) if cp := h.TxnEvents.capacity(); cp != 0 { t.Error("wrong txn event capacity", cp) } if cp := h.CustomEvents.capacity(); cp != 0 { t.Error("wrong custom event capacity", cp) } if cp := h.LogEvents.capacity(); cp != 0 { t.Error("wrong log event capacity", cp) } if cp := h.ErrorEvents.capacity(); cp != 0 { t.Error("wrong error event capacity", cp) } if cp := h.SpanEvents.capacity(); cp != 0 { t.Error("wrong error event capacity", cp) } // Add events to ensure that adding events to zero-capacity pools is // safe. h.TxnEvents.AddTxnEvent(&txnEvent{}, 1.0) h.CustomEvents.Add(&customEvent{}) h.LogEvents.Add(&logEvent{}) h.ErrorEvents.Add(&errorEvent{}, 1.0) h.SpanEvents.addEventPopulated(&sampleSpanEvent) // Create the payloads to ensure doing so with zero-capacity pools is // safe. payloads := h.Ready(now.Add(2 * time.Minute)).Payloads(false) for _, p := range payloads { js, err := p.Data("agentRunID", now.Add(2*time.Minute)) if nil != err { t.Error(err) continue } // Only metric data should be present. if (p.EndpointMethod() == "metric_data") != (string(js) != "") { t.Error(p.EndpointMethod(), string(js)) } } } go-agent-3.42.0/v3/newrelic/instrumentation.go000066400000000000000000000202521510742411500212340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "github.com/newrelic/go-agent/v3/internal" ) // instrumentation.go contains helpers built on the lower level api. // WrapHandle instruments http.Handler handlers with Transactions. To // instrument this code: // // http.Handle("/foo", myHandler) // // Perform this replacement: // // http.Handle(newrelic.WrapHandle(app, "/foo", myHandler)) // // WrapHandle adds the Transaction to the request's context. Access it using // FromContext to add attributes, create segments, or notice errors: // // func myHandler(rw ResponseWriter, req *Request) { // txn := newrelic.FromContext(req.Context()) // txn.AddAttribute("customerLevel", "gold") // io.WriteString(w, "users page") // } // // The WrapHandle function is safe to call if app is nil. // // WrapHandle accepts zero or more TraceOption functions to allow additional options to be // manually added to the transaction trace generated, in the same fashion as StartTransaction // does. For example, this can be used to control code level metrics generated for this transaction. func WrapHandle(app *Application, pattern string, handler http.Handler, options ...TraceOption) (string, http.Handler) { if app == nil { return pattern, handler } // add the wrapped function to the trace options as the source code reference point // (but only if we know we're collecting CLM for this transaction and the user didn't already // specify a different code location explicitly). cache := NewCachedCodeLocation() if IsSecurityAgentPresent() { secureAgent.SendEvent("API_END_POINTS", pattern, "*", internal.HandlerName(handler)) } return pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tOptions *traceOptSet var txnOptionList []TraceOption if app.app != nil { run, _ := app.app.getState() if run != nil && run.Config.CodeLevelMetrics.Enabled { tOptions = resolveCLMTraceOptions(options) if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || run.Config.CodeLevelMetrics.Scope == 0 || (run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { // we are for sure collecting CLM here, so go to the trouble of collecting this code location if nothing else has yet. if tOptions.LocationOverride == nil { if loc, err := cache.FunctionLocation(handler, handler.ServeHTTP); err == nil { WithCodeLocation(loc)(tOptions) } } } } } if tOptions == nil { // we weren't able to curate the options above, so pass whatever we were given downstream txnOptionList = options } else { txnOptionList = append(txnOptionList, withPreparedOptions(tOptions)) } txn := app.StartTransaction(r.Method+" "+pattern, txnOptionList...) defer txn.End() if IsSecurityAgentPresent() { txn.SetCsecAttributes(AttributeCsecRoute, pattern) } w = txn.SetWebResponse(w) txn.SetWebRequestHTTP(r) r = RequestWithTransactionContext(r, txn) handler.ServeHTTP(w, r) if IsSecurityAgentPresent() { secureAgent.SendEvent("RESPONSE_HEADER", w.Header(), txn.GetLinkingMetadata().TraceID) } }) } // AddCodeLevelMetricsTraceOptions adds trace options to an existing slice of TraceOption objects depending on how code level metrics is configured // in your application. // Please call cache:=newrelic.NewCachedCodeLocation() before calling this function, and pass the cache to us in order to allow you to optimize the // performance and accuracy of this function. func AddCodeLevelMetricsTraceOptions(app *Application, options []TraceOption, cache *CachedCodeLocation, cachedLocations ...interface{}) []TraceOption { var tOptions *traceOptSet var txnOptionList []TraceOption if cache == nil { return options } if app.app != nil { run, _ := app.app.getState() if run != nil && run.Config.CodeLevelMetrics.Enabled { tOptions = resolveCLMTraceOptions(options) if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || run.Config.CodeLevelMetrics.Scope == 0 || (run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { // we are for sure collecting CLM here, so go to the trouble of collecting this code location if nothing else has yet. if tOptions.LocationOverride == nil { if loc, err := cache.FunctionLocation(cachedLocations); err == nil { WithCodeLocation(loc)(tOptions) } } } } } if tOptions == nil { // we weren't able to curate the options above, so pass whatever we were given downstream txnOptionList = options } else { txnOptionList = append(txnOptionList, withPreparedOptions(tOptions)) } return txnOptionList } // WrapHandleFunc instruments handler functions using Transactions. To // instrument this code: // // http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { // io.WriteString(w, "users page") // }) // // Perform this replacement: // // http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", func(w http.ResponseWriter, req *http.Request) { // io.WriteString(w, "users page") // })) // // WrapHandleFunc adds the Transaction to the request's context. Access it using // FromContext to add attributes, create segments, or notice errors: // // http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", func(w http.ResponseWriter, req *http.Request) { // txn := newrelic.FromContext(req.Context()) // txn.AddAttribute("customerLevel", "gold") // io.WriteString(w, "users page") // })) // // The WrapHandleFunc function is safe to call if app is nil. // // WrapHandleFunc accepts zero or more TraceOption functions to allow additional options to be // manually added to the transaction trace generated, in the same fashion as StartTransaction // does. For example, this can be used to control code level metrics generated for this transaction. func WrapHandleFunc(app *Application, pattern string, handler func(http.ResponseWriter, *http.Request), options ...TraceOption) (string, func(http.ResponseWriter, *http.Request)) { // add the wrapped function to the trace options as the source code reference point // (to the beginning of the option list, so that the user can override this) p, h := WrapHandle(app, pattern, http.HandlerFunc(handler), options...) return p, func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) } } // WrapListen wraps an HTTP endpoint reference passed to functions like http.ListenAndServe, // which causes security scanning to be done for that incoming endpoint when vulnerability // scanning is enabled. It returns the endpoint string, so you can replace a call like // // http.ListenAndServe(":8000", nil) // // with // // http.ListenAndServe(newrelic.WrapListen(":8000"), nil) func WrapListen(endpoint string) string { if IsSecurityAgentPresent() { secureAgent.SendEvent("APP_INFO", endpoint) } return endpoint } // NewRoundTripper creates an http.RoundTripper to instrument external requests // and add distributed tracing headers. The http.RoundTripper returned creates // an external segment before delegating to the original http.RoundTripper // provided (or http.DefaultTransport if none is provided). The // http.RoundTripper will look for a Transaction in the request's context // (using FromContext). func NewRoundTripper(original http.RoundTripper) http.RoundTripper { if nil == original { original = http.DefaultTransport } return roundTripperFunc(func(request *http.Request) (*http.Response, error) { // The specification of http.RoundTripper requires that the request is never modified. request = cloneRequest(request) segment := StartExternalSegment(nil, request) response, err := original.RoundTrip(request) segment.Response = response segment.End() return response, err }) } // cloneRequest mimics implementation of // https://godoc.org/github.com/google/go-github/github#BasicAuthTransport.RoundTrip func cloneRequest(r *http.Request) *http.Request { // shallow copy of the struct r2 := new(http.Request) *r2 = *r // deep copy of the Header r2.Header = make(http.Header, len(r.Header)) for k, s := range r.Header { r2.Header[k] = append([]string(nil), s...) } return r2 } type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } go-agent-3.42.0/v3/newrelic/integrationsupport/000077500000000000000000000000001510742411500214215ustar00rootroot00000000000000go-agent-3.42.0/v3/newrelic/integrationsupport/example_test.go000066400000000000000000000071051510742411500244450ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package integrationsupport import ( "context" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" "net/http" "testing" "time" ) type myError struct{} func (e myError) Error() string { return "My error message" } func TestNewBasicTestApp(t *testing.T) { expectedApp := NewBasicTestApp() txn := expectedApp.Application.StartTransaction("MyTransaction") txn.NoticeError(myError{}) txn.End() expectedApp.ExpectErrors(t, []internal.WantError{{ Msg: "My error message", }}) } func TestDistributedTracingTestApp(t *testing.T) { expectedApp := NewTestApp(SampleEverythingReplyFn, DTEnabledCfgFn) txn := expectedApp.Application.StartTransaction("MyTransaction") client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) request, _ := http.NewRequest("GET", "https://example.com", nil) request = request.WithContext(newrelic.NewContext(context.Background(), txn)) _, err := client.Do(request) if err != nil { t.Fatal(err) return } time.Sleep(2 * time.Second) // Sleep to exceed trace threshold txn.End() expectedApp.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransaction/Go/MyTransaction", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/MyTransaction", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: "OtherTransaction/Go/MyTransaction", Forced: false, Data: nil}, }) expectedApp.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/MyTransaction", NumSegments: 1, }}) } func TestAppLogsTestApp(t *testing.T) { expectedApp := NewTestApp(SampleEverythingReplyFn, AppLogEnabledCfgFn) txn := expectedApp.Application.StartTransaction("MyTransaction") defer txn.End() txn.RecordLog(newrelic.LogData{ Message: "Transaction Log Message", Severity: "debug", Timestamp: 12345, }) expectedApp.RecordLog(newrelic.LogData{ Message: "App Log Message", Severity: "info", Timestamp: 78910, }) txn.ExpectLogEvents(t, []internal.WantLog{{ Message: "App Log Message", Severity: "info", Timestamp: 78910, SpanID: expectedApp.GetLinkingMetadata().SpanID, TraceID: expectedApp.GetLinkingMetadata().TraceID, Attributes: map[string]interface{}{}}, { Message: "Transaction Log Message", Severity: "debug", Timestamp: 12345, SpanID: txn.GetLinkingMetadata().SpanID, TraceID: txn.GetLinkingMetadata().TraceID, Attributes: map[string]interface{}{}}}) } func TestFullTracesTestApp(t *testing.T) { expectedApp := NewTestApp(SampleEverythingReplyFn, ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) txn := expectedApp.Application.StartTransaction("MyTransaction") AddAgentAttribute(txn, newrelic.AttributeSpanKind, "producer", nil) txn.End() expectedApp.ExpectTxnTraces(t, []internal.WantTxnTrace{ { AgentAttributes: map[string]interface{}{ newrelic.AttributeSpanKind: "producer", }, }, }) } go-agent-3.42.0/v3/newrelic/integrationsupport/integrationsupport.go000066400000000000000000000071701510742411500257350ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // Package integrationsupport exists to expose functionality to integration // packages without adding noise to the public API. package integrationsupport import ( "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) // AddAgentAttribute allows instrumentation packages to add agent attributes. func AddAgentAttribute(txn *newrelic.Transaction, id string, stringVal string, otherVal interface{}) { if nil == txn { return } if aa, ok := txn.Private.(internal.AddAgentAttributer); ok { aa.AddAgentAttribute(id, stringVal, otherVal) } } // AddAgentSpanAttribute allows instrumentation packages to add span attributes. func AddAgentSpanAttribute(txn *newrelic.Transaction, key string, val string) { if nil == txn { return } internal.AddAgentSpanAttribute(txn.Private, key, val) } // This code below is used for testing and is based on the similar code in internal_test.go in // the newrelic package. That code is not exported, though, and we frequently need something similar // for integration packages, so it is copied here. const ( testLicenseKey = "0123456789012345678901234567890123456789" SampleAppName = "my app" TestEntityGUID = "testEntityGUID123" ) // ExpectApp combines Application and Expect, for use in validating data in test apps type ExpectApp struct { internal.Expect *newrelic.Application } // ConfigFullTraces enables distributed tracing and sets transaction // trace and transaction trace segment thresholds to zero for full traces. func ConfigFullTraces(cfg *newrelic.Config) { cfg.DistributedTracer.Enabled = true cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 } // NewTestApp creates an ExpectApp with the given ConnectReply function and Config function func NewTestApp(replyfn func(*internal.ConnectReply), cfgFn ...newrelic.ConfigOption) ExpectApp { cfgFn = append(cfgFn, func(cfg *newrelic.Config) { // Prevent spawning app goroutines in tests. if !cfg.ServerlessMode.Enabled { cfg.Enabled = false } }, newrelic.ConfigAppName(SampleAppName), newrelic.ConfigLicense(testLicenseKey), ) app, err := newrelic.NewApplication(cfgFn...) if nil != err { panic(err) } internal.HarvestTesting(app.Private, replyfn) return ExpectApp{ Expect: app.Private.(internal.Expect), Application: app, } } // NewBasicTestApp creates an ExpectApp with the standard testing connect reply function and config func NewBasicTestApp() ExpectApp { return NewTestApp(nil, BasicConfigFn) } // BasicConfigFn is a default config function to be used when no special settings are needed for a test app var BasicConfigFn = func(cfg *newrelic.Config) { cfg.Enabled = false } // DTEnabledCfgFn is a reusable Config function that sets Distributed Tracing to enabled var DTEnabledCfgFn = func(cfg *newrelic.Config) { cfg.Enabled = false cfg.DistributedTracer.Enabled = true } // AppLogEnabledCfgFn enables application logging features var AppLogEnabledCfgFn = func(cfg *newrelic.Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = true cfg.ApplicationLogging.Metrics.Enabled = true } // SampleEverythingReplyFn is a reusable ConnectReply function that samples everything var SampleEverythingReplyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = TestEntityGUID reply.EventData = internal.DefaultEventHarvestConfig(internal.MaxTxnEvents, internal.MaxLogEvents, internal.MaxCustomEvents) } go-agent-3.42.0/v3/newrelic/integrationsupport/integrationsupport_test.go000066400000000000000000000053421510742411500267730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package integrationsupport import ( "sync" "testing" "github.com/newrelic/go-agent/v3/internal" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestNilTransaction(t *testing.T) { var txn *newrelic.Transaction AddAgentAttribute(txn, newrelic.AttributeHostDisplayName, "hostname", nil) AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, "operation") } func TestEmptyTransaction(t *testing.T) { txn := &newrelic.Transaction{} AddAgentAttribute(txn, newrelic.AttributeHostDisplayName, "hostname", nil) AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, "operation") } func testApp(t *testing.T) *newrelic.Application { app, err := newrelic.NewApplication( newrelic.ConfigAppName("appname"), newrelic.ConfigLicense("0123456789012345678901234567890123456789"), newrelic.ConfigEnabled(false), newrelic.ConfigDistributedTracerEnabled(true), newrelic.ConfigCodeLevelMetricsEnabled(false), ) if nil != err { t.Fatal(err) } replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } internal.HarvestTesting(app.Private, replyfn) return app } func TestSuccess(t *testing.T) { app := testApp(t) txn := app.StartTransaction("hello") AddAgentAttribute(txn, newrelic.AttributeHostDisplayName, "hostname", nil) segment := txn.StartSegment("mySegment") AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, "operation") segment.End() txn.End() app.Private.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ newrelic.AttributeHostDisplayName: "hostname", }, }, }) app.Private.(internal.Expect).ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/mySegment", "parentId": internal.MatchAnything, "category": "generic", }, AgentAttributes: map[string]interface{}{ newrelic.SpanAttributeAWSOperation: "operation", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "category": "generic", "nr.entryPoint": true, }, AgentAttributes: map[string]interface{}{ "host.displayName": "hostname", }, }, }) } func TestConcurrentCalls(t *testing.T) { // This test will fail with a data race if the txn is not properly locked app := testApp(t) txn := app.StartTransaction("hello") defer txn.End() defer txn.StartSegment("mySegment").End() var wg sync.WaitGroup addAttr := func() { AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, "operation") wg.Done() } wg.Add(1) go addAttr() wg.Wait() } go-agent-3.42.0/v3/newrelic/internal_17_test.go000066400000000000000000000220371510742411500211560ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" ) func myErrorHandler(w http.ResponseWriter, req *http.Request) { w.Write([]byte("my response")) // Ensure that the transaction is added to the request's context. txn := FromContext(req.Context()) txn.NoticeError(myError{}) } func TestWrapHandleFunc(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) mux := http.NewServeMux() mux.HandleFunc(WrapHandleFunc(app.Application, helloPath, myErrorHandler)) w := newCompatibleResponseRecorder() mux.ServeHTTP(w, helloRequest) out := w.Body.String() if "my response" != out { t.Error(out) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/GET /hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/GET /hello", }, AgentAttributes: mergeAttributes(helloRequestAttributes, map[string]interface{}{ "httpResponseCode": "200", "http.statusCode": "200", }), }}) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, }) } func TestWrapHandle(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) mux := http.NewServeMux() mux.Handle(WrapHandle(app.Application, helloPath, http.HandlerFunc(myErrorHandler))) w := newCompatibleResponseRecorder() mux.ServeHTTP(w, helloRequest) out := w.Body.String() if "my response" != out { t.Error(out) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/GET /hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/GET /hello", }, AgentAttributes: mergeAttributes(helloRequestAttributes, map[string]interface{}{ "httpResponseCode": "200", "http.statusCode": "200", }), }}) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, }) } func TestWrapHandleNilApp(t *testing.T) { var app *Application mux := http.NewServeMux() mux.Handle(WrapHandle(app, helloPath, http.HandlerFunc(myErrorHandler))) w := newCompatibleResponseRecorder() mux.ServeHTTP(w, helloRequest) out := w.Body.String() if "my response" != out { t.Error(out) } } func TestRoundTripper(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") url := "http://example.com/" req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatal(err) } req.Header.Add("zip", "zap") client := &http.Client{} inner := roundTripperFunc(func(r *http.Request) (*http.Response, error) { catHdr := r.Header.Get(DistributedTraceNewRelicHeader) if "" == catHdr { t.Error("cat header missing") } // Test that headers are preserved during reqest cloning: if z := r.Header.Get("zip"); z != "zap" { t.Error("missing header", z) } if r.URL.String() != url { t.Error(r.URL.String()) } return nil, errors.New("hello") }) req = RequestWithTransactionContext(req, txn) client.Transport = NewRoundTripper(inner) resp, err := client.Do(req) if resp != nil || err == nil { t.Error(resp, err.Error()) } // Ensure that the request was cloned: catHdr := req.Header.Get(DistributedTraceNewRelicHeader) if "" != catHdr { t.Error("cat header unexpectedly present") } txn.NoticeError(myError{}) txn.End() scope := "OtherTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: scope, Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Data: nil}, }, backgroundErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "spanId": "4981855ad8681d0d", "transactionName": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestRoundTripperOldCAT(t *testing.T) { cfgfn := func(c *Config) { c.DistributedTracer.Enabled = false c.CrossApplicationTracer.Enabled = true } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") url := "http://example.com/" req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatal(err) } client := &http.Client{} inner := roundTripperFunc(func(r *http.Request) (*http.Response, error) { // TODO test that request headers have been set here. if r.URL.String() != url { t.Error(r.URL.String()) } return nil, errors.New("hello") }) req = RequestWithTransactionContext(req, txn) client.Transport = NewRoundTripper(inner) resp, err := client.Do(req) if resp != nil || err == nil { t.Error(resp, err.Error()) } txn.NoticeError(myError{}) txn.End() scope := "OtherTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: scope, Forced: false, Data: nil}, }, backgroundErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, "nr.tripId": internal.MatchAnything, "nr.guid": internal.MatchAnything, "nr.pathHash": internal.MatchAnything, }, }}) } func TestRoundTripperRace(t *testing.T) { // Test to detect a potential data race when using NewRoundTripper in // multiple goroutines. client := &http.Client{ Transport: NewRoundTripper(nil), } req, _ := http.NewRequest("GET", "http://example.com", nil) go client.Do(req) go client.Do(req) } go-agent-3.42.0/v3/newrelic/internal_app.go000066400000000000000000000442271510742411500204550ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "compress/gzip" "errors" "fmt" "io" "math" "net/http" "os" "strings" "sync" "time" "github.com/newrelic/go-agent/v3/internal" ) type appData struct { id internal.AgentRunID data harvestable } type app struct { Logger config config rpmControls rpmControls testHarvest *harvest trObserver traceObserver // placeholderRun is used when the application is not connected. placeholderRun *appRun // initiateShutdown is used to tell the processor to shutdown. initiateShutdown chan time.Duration // shutdownStarted and shutdownComplete are closed by the processor // goroutine to indicate the shutdown status. Two channels are used so // that the call of app.Shutdown() can block until shutdown has // completed but other goroutines can exit when shutdown has started. // This is not just an optimization: This prevents a deadlock if // harvesting data during the shutdown fails and an attempt is made to // merge the data into the next harvest. shutdownStarted chan struct{} shutdownComplete chan struct{} // Sends to these channels should not occur without a <-shutdownStarted // select option to prevent deadlock. dataChan chan appData collectorErrorChan chan rpmResponse connectChan chan *appRun // This mutex protects both `run` and `err`, both of which should only // be accessed using getState and setState. sync.RWMutex // run is non-nil when the app is successfully connected. It is // immutable. run *appRun // err is non-nil if the application will never be connected again // (disconnect, license exception, shutdown). err error // registered callback functions llmTokenCountCallback func(string, string) int // high water mark alarms heapHighWaterMarkAlarms heapHighWaterMarkAlarmSet serverless *serverlessHarvest } func (app *app) doHarvest(h *harvest, harvestStart time.Time, run *appRun) { h.CreateFinalMetrics(run, app.getObserver()) payloads := h.Payloads(app.config.DistributedTracer.Enabled) for _, p := range payloads { cmd := p.EndpointMethod() var data []byte defer func() { if r := recover(); r != nil { app.Warn("panic occured when creating harvest data", map[string]interface{}{ "cmd": cmd, "panic": r, }) // make sure the loop continues data = nil } }() data, err := p.Data(run.Reply.RunID.String(), harvestStart) if err != nil { app.Warn("unable to create harvest data", map[string]interface{}{ "cmd": cmd, "error": err.Error(), }) continue } if data == nil { continue } call := rpmCmd{ Collector: run.Reply.Collector, RunID: run.Reply.RunID.String(), Name: cmd, Data: data, RequestHeadersMap: run.Reply.RequestHeadersMap, MaxPayloadSize: run.Reply.MaxPayloadSizeInBytes, } resp := collectorRequest(call, app.rpmControls) if resp.IsDisconnect() || resp.IsRestartException() { select { case app.collectorErrorChan <- *resp: case <-app.shutdownStarted: } return } if resp.GetError() != nil { app.Warn("harvest failure", map[string]interface{}{ "cmd": cmd, "error": resp.GetError().Error(), "retain_data": resp.ShouldSaveHarvestData(), }) } if resp.ShouldSaveHarvestData() { app.Consume(run.Reply.RunID, p) } } } func (app *app) connectRoutine() { attempts := 0 for { reply, resp := connectAttempt(app.config, app.rpmControls) if reply != nil { select { case app.connectChan <- newAppRun(app.config, reply): case <-app.shutdownStarted: } return } if resp.IsDisconnect() { select { case app.collectorErrorChan <- *resp: case <-app.shutdownStarted: } return } if nil != resp.GetError() { app.Warn("application connect failure", map[string]interface{}{ "error": resp.GetError().Error(), }) } backoff := getConnectBackoffTime(attempts) time.Sleep(time.Duration(backoff) * time.Second) attempts++ } } func (app *app) connectTraceObserver(reply *internal.ConnectReply) { if obs := app.getObserver(); obs != nil { obs.restart(reply.RunID, reply.RequestHeadersMap) return } var endpoint observerURL if nil != app.config.traceObserverURL { endpoint = *app.config.traceObserverURL } observer, err := newTraceObserver(reply.RunID, reply.RequestHeadersMap, observerConfig{ endpoint: endpoint, license: app.config.License, log: app.config.Logger, queueSize: app.config.InfiniteTracing.SpanEvents.QueueSize, appShutdown: app.shutdownComplete, dialer: reply.TraceObsDialer, }) if nil != err { app.Error("unable to create trace observer", map[string]interface{}{ "err": err.Error(), }) return } app.Debug("trace observer connected", map[string]interface{}{ "url": app.config.traceObserverURL.host, }) app.setObserver(observer) } // Connect backoff time follows the sequence defined at // https://source.datanerd.us/agents/agent-specs/blob/master/Collector-Response-Handling.md#retries-and-backoffs func getConnectBackoffTime(attempt int) int { connectBackoffTimes := [...]int{15, 15, 30, 60, 120, 300} l := len(connectBackoffTimes) if (attempt < 0) || (attempt >= l) { return connectBackoffTimes[l-1] } return connectBackoffTimes[attempt] } func processConnectMessages(run *appRun, lg Logger) { for _, msg := range run.Reply.Messages { event := "collector message" cn := map[string]interface{}{"msg": msg.Message} switch strings.ToLower(msg.Level) { case "error": lg.Error(event, cn) case "warn": lg.Warn(event, cn) case "info": lg.Info(event, cn) case "debug", "verbose": lg.Debug(event, cn) } } } func (app *app) process() { // Both the harvest and the run are non-nil when the app is connected, // and nil otherwise. var h *harvest var run *appRun harvestTicker := time.NewTicker(time.Second) defer harvestTicker.Stop() for { select { case <-harvestTicker.C: if nil != run { now := time.Now() if ready := h.Ready(now); nil != ready { go app.doHarvest(ready, now, run) } } case d := <-app.dataChan: if nil != run && run.Reply.RunID == d.id { d.data.MergeIntoHarvest(h) } case timeout := <-app.initiateShutdown: close(app.shutdownStarted) // Remove the run before merging any final data to // ensure a bounded number of receives from dataChan. app.setState(nil, errApplicationShutDown) if obs := app.getObserver(); obs != nil { if err := obs.shutdown(timeout); err != nil { app.Error("trace observer shutdown timeout exceeded", map[string]interface{}{ "err": err.Error(), }) } } if nil != run { for done := false; !done; { select { case d := <-app.dataChan: if run.Reply.RunID == d.id { d.data.MergeIntoHarvest(h) } default: done = true } } app.doHarvest(h, time.Now(), run) } close(app.shutdownComplete) app.setObserver(nil) secureAgent.DeactivateSecurity() return case resp := <-app.collectorErrorChan: run = nil h = nil app.setState(nil, nil) if resp.IsDisconnect() { app.setState(nil, resp.GetError()) app.Error("application disconnected", map[string]interface{}{ "app": app.config.AppName, }) secureAgent.DeactivateSecurity() } else if resp.IsRestartException() { app.Info("application restarted", map[string]interface{}{ "app": app.config.AppName, }) go app.connectRoutine() } case run = <-app.connectChan: if shouldUseTraceObserver(run.Config) { app.connectTraceObserver(run.Reply) } else if shouldUseTraceObserver(app.config) { app.Debug("trace observer disabled via backend", map[string]interface{}{ "local-DistributedTracer.Enabled": app.config.DistributedTracer.Enabled, "server-DistributedTracer.Enabled": run.Config.DistributedTracer.Enabled, "local-SpanEvents.Enabled": app.config.SpanEvents.Enabled, "server-SpanEvents.Enabled": run.Config.SpanEvents.Enabled, }) } run.harvestConfig.CommonAttributes = commonAttributes{ hostname: app.config.hostname, entityName: app.config.AppName, entityGUID: run.Reply.EntityGUID, } h = newHarvest(time.Now(), run.harvestConfig) app.setState(run, nil) app.Info("application connected", map[string]interface{}{ "app": app.config.AppName, "run": run.Reply.RunID.String(), }) processConnectMessages(run, app) secureAgent.RefreshState(getLinkedMetaData(app)) } } } func (app *app) Shutdown(timeout time.Duration) { if nil == app { return } if !app.config.Enabled { return } if app.config.ServerlessMode.Enabled { return } select { case app.initiateShutdown <- timeout: default: } // Block until shutdown is done or timeout occurs. t := time.NewTimer(timeout) select { case <-app.shutdownComplete: case <-t.C: } t.Stop() app.Info("application shutdown", map[string]interface{}{ "app": app.config.AppName, }) } func runSampler(app *app, period time.Duration) { previous := getSystemSample(time.Now(), app) t := time.NewTicker(period) for { select { case now := <-t.C: current := getSystemSample(now, app) run, _ := app.getState() app.Consume(run.Reply.RunID, getSystemStats(systemSamples{ Previous: previous, Current: current, })) previous = current case <-app.shutdownStarted: t.Stop() return } } } func (app *app) WaitForConnection(timeout time.Duration) error { if nil == app { return nil } if !app.config.Enabled { return nil } if app.config.ServerlessMode.Enabled { return nil } deadline := time.Now().Add(timeout) pollPeriod := 50 * time.Millisecond for { run, err := app.getState() if nil != err { return err } if run.Reply.RunID != "" { if shouldUseTraceObserver(run.Config) { if obs := app.getObserver(); obs != nil && obs.initialConnCompleted() { return nil } } else { return nil } } if time.Now().After(deadline) { return fmt.Errorf("timeout out after %s", timeout.String()) } time.Sleep(pollPeriod) } } func newApp(c config) *app { transport := c.Transport if nil == transport { transport = collectorDefaultTransport } app := &app{ Logger: c.Logger, config: c, placeholderRun: newPlaceholderAppRun(c), // This channel must be buffered since Shutdown makes a // non-blocking send attempt. initiateShutdown: make(chan time.Duration, 1), shutdownStarted: make(chan struct{}), shutdownComplete: make(chan struct{}), connectChan: make(chan *appRun, 1), collectorErrorChan: make(chan rpmResponse, 1), dataChan: make(chan appData, appDataChanSize), rpmControls: rpmControls{ License: c.License, Client: &http.Client{ Transport: transport, Timeout: collectorTimeout, }, Logger: c.Logger, GzipWriterPool: &sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, }, }, } app.Info("application created", map[string]interface{}{ "app": app.config.AppName, "version": Version, "enabled": app.config.Enabled, "grpc-version": grpcVersion, }) if app.config.Enabled { if app.config.ServerlessMode.Enabled { reply := newServerlessConnectReply(c) app.run = newAppRun(c, reply) app.serverless = newServerlessHarvest(c.Logger, os.Getenv) } else { go app.process() go app.connectRoutine() if app.config.RuntimeSampler.Enabled { go runSampler(app, runtimeSamplerPeriod) } } } return app } func shouldUseTraceObserver(c config) bool { return nil != c.traceObserverURL && c.SpanEvents.Enabled && c.DistributedTracer.Enabled } var ( _ internal.HarvestTestinger = &app{} _ internal.Expect = &app{} ) func (app *app) HarvestTesting(replyfn func(*internal.ConnectReply)) { if nil != replyfn { reply := internal.ConnectReplyDefaults() replyfn(reply) app.placeholderRun = newAppRun(app.config, reply) } app.testHarvest = newHarvest(time.Now(), app.placeholderRun.harvestConfig) } func (app *app) getState() (*appRun, error) { app.RLock() defer app.RUnlock() run := app.run if nil == run { run = app.placeholderRun } return run, app.err } func (app *app) setState(run *appRun, err error) { app.Lock() defer app.Unlock() app.run = run app.err = err } func (app *app) getObserver() traceObserver { app.RLock() defer app.RUnlock() return app.trObserver } func (app *app) setObserver(observer traceObserver) { app.Lock() defer app.Unlock() app.trObserver = observer } func newTransaction(thd *thread) *Transaction { return &Transaction{ Private: thd, thread: thd, } } // StartTransaction implements newrelic.Application's StartTransaction. func (app *app) StartTransaction(name string, opts ...TraceOption) *Transaction { if nil == app { return nil } run, _ := app.getState() return newTransaction(newTxn(app, run, name, opts...)) } var ( errHighSecurityEnabled = errors.New("high security enabled") errCustomEventsDisabled = errors.New("custom events disabled") errCustomEventsRemoteDisabled = errors.New("custom events disabled by server") errApplicationShutDown = errors.New("application shut down") ) // RecordCustomEvent implements newrelic.Application's RecordCustomEvent. func (app *app) RecordCustomEvent(eventType string, params map[string]interface{}) error { var event *customEvent var e error if nil == app { return nil } if app.config.Config.HighSecurity { return errHighSecurityEnabled } if !app.config.CustomInsightsEvents.Enabled { return errCustomEventsDisabled } if eventType == "LlmEmbedding" || eventType == "LlmChatCompletionSummary" || eventType == "LlmChatCompletionMessage" { event, e = createCustomEventUnlimitedSize(eventType, params, time.Now()) } else { event, e = createCustomEvent(eventType, params, time.Now()) } if nil != e { return e } run, _ := app.getState() if !run.Reply.CollectCustomEvents { return errCustomEventsRemoteDisabled } if !run.Reply.SecurityPolicies.CustomEvents.Enabled() { return errSecurityPolicy } app.Consume(run.Reply.RunID, event) return nil } var ( errMetricInf = errors.New("invalid metric value: inf") errMetricNaN = errors.New("invalid metric value: NaN") errMetricNameEmpty = errors.New("missing metric name") errMetricServerless = errors.New("custom metrics are not currently supported in serverless mode") ) // RecordCustomMetric implements newrelic.Application's RecordCustomMetric. func (app *app) RecordCustomMetric(name string, value float64) error { if nil == app { return nil } if app.config.ServerlessMode.Enabled { return errMetricServerless } if math.IsNaN(value) { return errMetricNaN } if math.IsInf(value, 0) { return errMetricInf } if name == "" { return errMetricNameEmpty } run, _ := app.getState() app.Consume(run.Reply.RunID, customMetric{ RawInputName: name, Value: value, }) return nil } var ( errAppLoggingDisabled = errors.New("log data can not be recorded when application logging is disabled") ) // RecordLog implements newrelic.Application's RecordLog. func (app *app) RecordLog(log *LogData) error { if !app.config.ApplicationLogging.Enabled { return errAppLoggingDisabled } event, err := log.toLogEvent() if err != nil { return err } run, _ := app.getState() app.Consume(run.Reply.RunID, &event) return nil } var ( _ internal.ServerlessWriter = &app{} ) func (app *app) ServerlessWrite(arn string, writer io.Writer) { app.serverless.Write(arn, writer) } func (app *app) Consume(id internal.AgentRunID, data harvestable) { app.serverless.Consume(data) if nil != app.testHarvest { data.MergeIntoHarvest(app.testHarvest) return } if id == "" { return } select { case app.dataChan <- appData{id, data}: case <-app.shutdownStarted: } } func (app *app) ExpectCustomEvents(t internal.Validator, want []internal.WantEvent) { expectCustomEvents(extendValidator(t, "custom events"), app.testHarvest.CustomEvents, want) } // ExpectLogEvents from app checks that the contents of the logs test harvest matches the list of WantLogs. func (app *app) ExpectLogEvents(t internal.Validator, want []internal.WantLog) { expectLogEvents(extendValidator(t, "log events"), app.testHarvest.LogEvents, want) } // ExpectLogEvents from transactions dumps all the log events from a transaction into the test harvest // then checks that the contents of the logs harvest matches the list of WantLogs. func (txn *Transaction) ExpectLogEvents(t internal.Validator, want []internal.WantLog) { txn.thread.MergeIntoHarvest(txn.Application().app.testHarvest) expectLogEvents(extendValidator(t, "log events"), txn.Application().app.testHarvest.LogEvents, want) } func (app *app) ExpectErrors(t internal.Validator, want []internal.WantError) { t = extendValidator(t, "traced errors") expectErrors(t, app.testHarvest.ErrorTraces, want) } func (app *app) ExpectErrorEvents(t internal.Validator, want []internal.WantEvent) { t = extendValidator(t, "error events") expectErrorEvents(t, app.testHarvest.ErrorEvents, want) } func (app *app) ExpectSpanEvents(t internal.Validator, want []internal.WantEvent) { t = extendValidator(t, "spans events") expectSpanEvents(t, app.testHarvest.SpanEvents, want) } func (app *app) ExpectTxnEvents(t internal.Validator, want []internal.WantEvent) { t = extendValidator(t, "txn events") expectTxnEvents(t, app.testHarvest.TxnEvents, want) } func (app *app) ExpectMetrics(t internal.Validator, want []internal.WantMetric) { t = extendValidator(t, "metrics") expectMetrics(t, app.testHarvest.Metrics, want) } func (app *app) ExpectMetricsPresent(t internal.Validator, want []internal.WantMetric) { t = extendValidator(t, "metrics") expectMetricsPresent(t, app.testHarvest.Metrics, want) } func (app *app) ExpectTxnMetrics(t internal.Validator, want internal.WantTxn) { t = extendValidator(t, "metrics") expectTxnMetrics(t, app.testHarvest.Metrics, want) } func (app *app) ExpectTxnTraces(t internal.Validator, want []internal.WantTxnTrace) { t = extendValidator(t, "txn traces") expectTxnTraces(t, app.testHarvest.TxnTraces, want) } func (app *app) ExpectSlowQueries(t internal.Validator, want []internal.WantSlowQuery) { t = extendValidator(t, "slow queries") expectSlowQueries(t, app.testHarvest.SlowSQLs, want) } go-agent-3.42.0/v3/newrelic/internal_app_test.go000066400000000000000000001116731510742411500215140ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "fmt" "net/http" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" ) // some of these tests were initially generated by Github Copilot and/or TabNine and then tweaked by a human func TestConnectTraceObserver_RestartCalled(t *testing.T) { app := &app{} mockObs := &mockTraceObserver{} app.setObserver(mockObs) reply := &internal.ConnectReply{ RunID: internal.AgentRunID("test-run-id"), RequestHeadersMap: map[string]string{"header": "value"}, } app.connectTraceObserver(reply) if !mockObs.restartCalled { t.Error("expected restart to be called on observer") } if mockObs.lastRunID != reply.RunID { t.Errorf("expected RunID %v, got %v", reply.RunID, mockObs.lastRunID) } if mockObs.lastHeaders["header"] != "value" { t.Errorf("expected header value 'value', got %v", mockObs.lastHeaders["header"]) } } func TestConnectBackoff(t *testing.T) { attempts := map[int]int{ 0: 15, 2: 30, 5: 300, 6: 300, 100: 300, -5: 300, } for k, v := range attempts { if b := getConnectBackoffTime(k); b != v { t.Error(fmt.Sprintf("Invalid connect backoff for attempt #%d:", k), v) } } } func TestProcessConnectMessages(t *testing.T) { testCases := []struct { name string level string message string expectedMethod string }{ { name: "error level", level: "ERROR", message: "An error occurred", expectedMethod: "Error", }, { name: "warn level", level: "WARN", message: "A warning occurred", expectedMethod: "Warn", }, { name: "info level", level: "INFO", message: "An info message", expectedMethod: "Info", }, { name: "debug level", level: "DEBUG", message: "A debug message", expectedMethod: "Debug", }, { name: "verbose level", level: "VERBOSE", message: "A verbose message", expectedMethod: "Debug", }, { name: "unknown level", level: "unknown", message: "An unknown level message", expectedMethod: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tl := &testLoggerWithMethodTracking{} run := &appRun{ Reply: &internal.ConnectReply{ Messages: []struct { Message string `json:"message"` Level string `json:"level"` }{ { Message: tc.message, Level: tc.level, }, }, }, } processConnectMessages(run, tl) if tc.expectedMethod == "" { // For unknown levels, no method should be called if len(tl.calledMethods) != 0 { t.Errorf("Expected no logger methods to be called for unknown level, but got: %v", tl.calledMethods) } } else { // Check that the correct method was called if len(tl.calledMethods) != 1 { t.Errorf("Expected exactly one logger method to be called, but got: %v", tl.calledMethods) } else if tl.calledMethods[0] != tc.expectedMethod { t.Errorf("Expected %s method to be called, but got: %s", tc.expectedMethod, tl.calledMethods[0]) } // Check that the message was logged correctly if len(tl.messages) != 1 { t.Errorf("Expected exactly one message to be logged, but got: %v", tl.messages) } else if tl.messages[0] != "collector message" { t.Errorf("Expected 'collector message' to be logged, but got: %s", tl.messages[0]) } // Check that the fields contain the original message if len(tl.fields) != 1 { t.Errorf("Expected exactly one field map, but got: %v", tl.fields) } else if msg, ok := tl.fields[0]["msg"]; !ok || msg != tc.message { t.Errorf("Expected field 'msg' to be '%s', but got: %v", tc.message, msg) } } }) } } func TestProcess(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true }) app := testApp.Application.app app.config.Enabled = true // Ensure all channels are initialized if app.dataChan == nil { app.dataChan = make(chan appData, 1) } if app.initiateShutdown == nil { app.initiateShutdown = make(chan time.Duration, 1) } if app.collectorErrorChan == nil { app.collectorErrorChan = make(chan rpmResponse, 1) } if app.connectChan == nil { app.connectChan = make(chan *appRun, 1) } if app.shutdownStarted == nil { app.shutdownStarted = make(chan struct{}) } if app.shutdownComplete == nil { app.shutdownComplete = make(chan struct{}) } run := &appRun{ Reply: &internal.ConnectReply{ Messages: []struct { Message string `json:"message"` Level string `json:"level"` }{ { Message: "test message", Level: "INFO", }, }, RunID: "test-run-id", }, } app.run = run harvest := newHarvest(time.Now(), run.harvestConfig) processDone := make(chan struct{}) go func() { app.process() close(processDone) }() // Case 1: Harvest ticker triggers harvestReady := make(chan struct{}) go func() { harvest.Ready(time.Now()) close(harvestReady) }() select { case <-harvestReady: case <-time.After(500 * time.Millisecond): t.Error("harvest did not become ready in time") } // Case 2: Data channel receives data dataSent := make(chan struct{}) go func() { app.dataChan <- appData{ id: run.Reply.RunID, data: customMetric{RawInputName: "testMetric", Value: 42}, } close(dataSent) }() select { case <-dataSent: case <-time.After(500 * time.Millisecond): t.Error("data was not sent in time") } // Case 3: Shutdown initiated shutdownInitiated := make(chan struct{}) go func() { app.initiateShutdown <- 1 * time.Second close(shutdownInitiated) }() select { case <-shutdownInitiated: case <-time.After(500 * time.Millisecond): t.Error("shutdown was not initiated in time") } // Case 4: Collector error channel receives a response collectorErrorSent := make(chan struct{}) go func() { app.collectorErrorChan <- rpmResponse{statusCode: 410, err: errors.New("disconnect")} close(collectorErrorSent) }() select { case <-collectorErrorSent: case <-time.After(500 * time.Millisecond): t.Error("collector error was not sent in time") } // Case 5: Connect channel receives a new run connectSent := make(chan struct{}) go func() { app.connectChan <- newAppRun(app.config, run.Reply) close(connectSent) }() select { case <-connectSent: case <-time.After(500 * time.Millisecond): t.Error("connect was not sent in time") } select { case <-processDone: case <-time.After(2 * time.Second): t.Error("process did not finish in time") } app.Shutdown(1 * time.Second) } func TestProcess_HarvestAndShutdown(t *testing.T) { // Setup a test app with enabled config app := newTestApp( func(reply *internal.ConnectReply) { reply.RunID = "test-run-id" reply.CollectCustomEvents = true reply.SecurityPolicies.CustomEvents.SetEnabled(true) }, func(cfg *Config) { cfg.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, ) // Start the app's process in a goroutine go app.Application.app.process() // Send a custom event to trigger dataChan case app.Application.RecordCustomEvent("TestEvent", map[string]interface{}{"foo": "bar"}) // Initiate shutdown timeout := 100 * time.Millisecond app.Application.app.initiateShutdown <- timeout // Wait for shutdown to complete select { case <-app.Application.app.shutdownComplete: case <-time.After(2 * timeout): t.Fatal("shutdown did not complete in time") } } func TestAppProcess_CollectorErrorChan_Disconnect(t *testing.T) { app := newTestApp( func(reply *internal.ConnectReply) { reply.RunID = internal.AgentRunID("test-run-id") }, func(cfg *Config) { cfg.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, ) go app.Application.app.process() // Send a disconnect error to collectorErrorChan resp := rpmResponse{statusCode: 410, err: errors.New("disconnect")} app.Application.app.collectorErrorChan <- resp // Wait for shutdown to complete select { case <-app.Application.app.shutdownComplete: case <-time.After(200 * time.Millisecond): // Not all disconnects will close shutdownComplete, so just check state if app.Application.app.run != nil { t.Error("app run should be nil after disconnect") } } } func TestAppProcess_CollectorErrorChan_RestartException(t *testing.T) { app := newTestApp( func(reply *internal.ConnectReply) { reply.RunID = internal.AgentRunID("test-run-id") }, func(cfg *Config) { cfg.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, ) go app.Application.app.process() // Send a restart exception error (statusCode 401) to collectorErrorChan resp := rpmResponse{statusCode: 401, err: errors.New("restart exception")} app.Application.app.collectorErrorChan <- resp // Wait for shutdown to complete select { case <-app.Application.app.shutdownComplete: case <-time.After(200 * time.Millisecond): // Not all restart exceptions will close shutdownComplete, so just check state if app.Application.app.run != nil { t.Error("app run should be nil after restart exception") } } } func TestAppProcess_ConnectChan_TraceObserverVariants(t *testing.T) { type testCase struct { name string setRunTraceObserver bool checkConfig func(app expectApplication, run *appRun) bool expectShouldUse bool } testCases := []testCase{ { name: "should use trace observer from app config", setRunTraceObserver: false, checkConfig: func(app expectApplication, run *appRun) bool { return shouldUseTraceObserver(app.app.config) }, expectShouldUse: true, }, { name: "should use trace observer from run config", setRunTraceObserver: true, checkConfig: func(app expectApplication, run *appRun) bool { return shouldUseTraceObserver(run.Config) }, expectShouldUse: true, }, { name: "should use trace observer from app config only", setRunTraceObserver: false, checkConfig: func(app expectApplication, run *appRun) bool { return shouldUseTraceObserver(app.app.config) }, expectShouldUse: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testApp := newTestApp( func(reply *internal.ConnectReply) { reply.RunID = internal.AgentRunID("test-run-id") reply.CollectCustomEvents = true reply.SecurityPolicies.CustomEvents.SetEnabled(true) }, func(cfg *Config) { cfg.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = true }, ) testApp.app.config.traceObserverURL = &observerURL{host: "localhost"} go testApp.app.process() run := testApp.app.placeholderRun if tc.setRunTraceObserver { run.Config.traceObserverURL = &observerURL{host: "localhost"} } testApp.app.connectChan <- run if got := tc.checkConfig(testApp, run); got != tc.expectShouldUse { t.Errorf("shouldUseTraceObserver returned %v, want %v", got, tc.expectShouldUse) } timeout := 100 * time.Millisecond testApp.app.initiateShutdown <- timeout select { case <-testApp.app.shutdownComplete: case <-time.After(2 * timeout): t.Fatal("shutdown did not complete in time") } }) } } func TestShutdownCoreLogic(t *testing.T) { t.Run("shutdown with nil app", func(t *testing.T) { app := &Application{} start := time.Now() app.app.Shutdown(2 * time.Second) elapsed := time.Since(start) if elapsed > 10*time.Millisecond { t.Errorf("Shutdown took too long for nil app: %v", elapsed) } }) t.Run("shutdown with config disabled", func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = false }) start := time.Now() testApp.app.Shutdown(2 * time.Second) elapsed := time.Since(start) if elapsed > 10*time.Millisecond { t.Errorf("Shutdown took too long for disabled config: %v", elapsed) } }) t.Run("shutdown with serverless", func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true cfg.ServerlessMode.Enabled = true }) start := time.Now() testApp.app.Shutdown(2 * time.Second) elapsed := time.Since(start) if elapsed > 10*time.Millisecond { t.Errorf("Shutdown took too long for serverless app: %v", elapsed) } }) t.Run("shutdown with running app", func(t *testing.T) { tl := &testLogger{} testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true cfg.Logger = tl }) testApp.app.config.Enabled = true time.Sleep(100 * time.Millisecond) start := time.Now() testApp.app.Shutdown(50 * time.Millisecond) elapsed := time.Since(start) if elapsed < 50*time.Millisecond || elapsed > 150*time.Millisecond { t.Errorf("Expected timeout around 50ms, got %v", elapsed) } found := false for _, msg := range tl.messages { if msg == "application shutdown" { found = true break } } if !found { t.Error("Expected 'application shutdown' log message not found") } }) t.Run("shutdown timeout case - timer expires", func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true }) time.Sleep(100 * time.Millisecond) testApp.app.config.Enabled = true start := time.Now() testApp.app.Shutdown(1 * time.Millisecond) elapsed := time.Since(start) if elapsed < 1*time.Millisecond || elapsed > 50*time.Millisecond { t.Errorf("Expected timeout around 1ms, got %v", elapsed) } }) t.Run("multiple shutdown calls - default case coverage", func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true }) time.Sleep(100 * time.Millisecond) done1 := make(chan bool) go func() { testApp.app.config.Enabled = true testApp.app.Shutdown(2 * time.Second) done1 <- true }() time.Sleep(10 * time.Millisecond) done2 := make(chan bool) go func() { testApp.app.config.Enabled = true testApp.app.Shutdown(1 * time.Second) done2 <- true }() select { case <-done1: case <-time.After(3 * time.Second): t.Error("First shutdown timed out") } select { case <-done2: case <-time.After(2 * time.Second): t.Error("Second shutdown timed out") } }) } func TestRunSampler(t *testing.T) { t.Run("run sampler with shutdown", func(t *testing.T) { testApp := newTestApp(sampleEverythingReplyFn, configTestAppLogFn) app := testApp.app app.config.RuntimeSampler.Enabled = true // Create a channel to track when sampler exits samplerDone := make(chan struct{}) // Start the sampler in a goroutine go func() { defer close(samplerDone) runSampler(app, 50*time.Millisecond) // Short period for testing }() // Let it run for a bit time.Sleep(100 * time.Millisecond) // Trigger shutdown close(app.shutdownStarted) // Wait for sampler to exit select { case <-samplerDone: // Success - sampler exited case <-time.After(time.Second): t.Error("sampler did not exit after shutdown signal") } }) t.Run("run sampler collects data", func(t *testing.T) { testApp := newTestApp(sampleEverythingReplyFn, configTestAppSamplerFn) app := testApp.app // Initialize test harvest so Consume works properly app.HarvestTesting(nil) // Ensure the app has a valid run state with RunID run, _ := app.getState() if run.Reply.RunID == "" { // Set a test run ID so Consume works reply := *run.Reply reply.RunID = "test-run-id" testRun := &appRun{ Reply: &reply, Config: run.Config, harvestConfig: run.harvestConfig, } app.setState(testRun, nil) } // Start the sampler go func() { runSampler(app, 50*time.Millisecond) }() // Wait for data to be collected by checking if any metrics exist ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: // Check for any system metrics by trying to validate an empty list // If metrics exist, ExpectMetricsPresent won't call Error app.ExpectMetricsPresent(testApp, []internal.WantMetric{}) app.ExpectTxnMetrics(testApp, internal.WantTxn{}) // If we get here without errors, metrics were found close(app.shutdownStarted) return case <-time.After(250 * time.Millisecond): t.Error("no system stats data received") close(app.shutdownStarted) return } } }) } func TestWaitForConnection(t *testing.T) { t.Run("early returns", func(t *testing.T) { testCases := []struct { name string app *app config func(*Config) }{ {"nil app", nil, nil}, {"disabled app", nil, func(cfg *Config) { cfg.Enabled = false }}, {"serverless app", nil, func(cfg *Config) { cfg.ServerlessMode.Enabled = true }}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var app *app if tc.config != nil { testApp := newTestApp(nil, tc.config) app = testApp.app } err := app.WaitForConnection(1 * time.Second) if err != nil { t.Errorf("Expected nil error, got: %v", err) } app.Shutdown(1 * time.Second) }) } }) t.Run("timeout cases", func(t *testing.T) { testCases := []struct { name string setupFn func(*app) }{ { "no connection", func(app *app) { app.config.Enabled = true }, }, { "empty run id", func(app *app) { app.config.Enabled = true run := app.placeholderRun reply := *run.Reply reply.RunID = "" app.setState(&appRun{Reply: &reply, Config: run.Config, harvestConfig: run.harvestConfig}, nil) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true }) tc.setupFn(testApp.app) start := time.Now() err := testApp.app.WaitForConnection(100 * time.Millisecond) elapsed := time.Since(start) if err == nil || !strings.Contains(err.Error(), "timeout") { t.Errorf("Expected timeout error, got: %v", err) } if elapsed < 90*time.Millisecond || elapsed > 200*time.Millisecond { t.Errorf("Expected elapsed time around 100ms, got: %v", elapsed) } testApp.Shutdown(10 * time.Second) }) } }) t.Run("successful connections", func(t *testing.T) { testCases := []struct { name string replyFn func(*internal.ConnectReply) configFn func(*Config) setupObserver bool observerConnected bool }{ { "without trace observer", func(reply *internal.ConnectReply) { reply.RunID = "test-run-123" }, func(cfg *Config) { cfg.Enabled = true }, false, false, }, { "with connected trace observer", func(reply *internal.ConnectReply) { reply.RunID = "test-run-456" }, func(cfg *Config) { cfg.Enabled = true cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = true }, true, true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testApp := newTestApp(tc.replyFn, tc.configFn) testApp.app.config.Enabled = true if tc.setupObserver { testApp.app.setObserver(&mockTraceObserver{connected: tc.observerConnected}) } // Setup connected state run := testApp.app.placeholderRun reply := *run.Reply config := run.Config if tc.setupObserver { config.DistributedTracer.Enabled = true config.SpanEvents.Enabled = true } testApp.app.setState(&appRun{ Reply: &reply, Config: config, harvestConfig: run.harvestConfig, }, nil) err := testApp.app.WaitForConnection(1 * time.Second) if err != nil { t.Errorf("Expected nil error, got: %v", err) } testApp.Shutdown(1 * time.Second) }) } }) t.Run("app error state", func(t *testing.T) { testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true }) testApp.app.config.Enabled = true expectedErr := errors.New("connection failed") testApp.app.setState(nil, expectedErr) err := testApp.app.WaitForConnection(1 * time.Second) if !errors.Is(err, expectedErr) { t.Errorf("Expected error %v, got: %v", expectedErr, err) } testApp.Shutdown(1 * time.Second) }) } func TestNewApp(t *testing.T) { testCases := []struct { name string configFn func(*Config) expectEnabled bool expectChannels bool expectLogger bool }{ { "disabled app", func(cfg *Config) { cfg.Enabled = false cfg.AppName = "test-app" cfg.License = testLicenseKey }, false, true, true, }, { "enabled app", func(cfg *Config) { cfg.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, true, true, true, }, { "serverless app", func(cfg *Config) { cfg.Enabled = true cfg.ServerlessMode.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, true, true, true, }, { "runtime sampler enabled", func(cfg *Config) { cfg.Enabled = true cfg.RuntimeSampler.Enabled = true cfg.AppName = "test-app" cfg.License = testLicenseKey }, true, true, true, }, { "custom transport", func(cfg *Config) { cfg.Transport = &http.Transport{} cfg.AppName = "test-app" cfg.License = testLicenseKey }, false, true, true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cfg := &Config{ AppName: "", License: "", Logger: new(testLogger), } tc.configFn(cfg) app := newApp(config{Config: *cfg}) if app == nil { t.Fatal("Expected non-nil app") } if app.config.Enabled != tc.expectEnabled { t.Errorf("Expected enabled=%v, got %v", tc.expectEnabled, app.config.Enabled) } if tc.expectChannels { if app.initiateShutdown == nil { t.Error("Expected initiateShutdown channel to be initialized") } if app.shutdownStarted == nil { t.Error("Expected shutdownStarted channel to be initialized") } if app.shutdownComplete == nil { t.Error("Expected shutdownComplete channel to be initialized") } if app.connectChan == nil { t.Error("Expected connectChan to be initialized") } if app.collectorErrorChan == nil { t.Error("Expected collectorErrorChan to be initialized") } if app.dataChan == nil { t.Error("Expected dataChan to be initialized") } } if tc.expectLogger && app.Logger == nil { t.Error("Expected logger to be set") } if app.placeholderRun == nil { t.Error("Expected placeholderRun to be initialized") } if app.rpmControls.Client == nil { t.Error("Expected HTTP client to be initialized") } if app.rpmControls.GzipWriterPool == nil { t.Error("Expected gzip writer pool to be initialized") } if cfg.ServerlessMode.Enabled && app.config.Enabled { if app.serverless == nil { t.Error("Expected serverless harvest to be initialized") } if app.run == nil { t.Error("Expected run to be set for serverless mode") } } if tc.configFn != nil && app.config.Transport == nil { if app.rpmControls.Client.Transport != collectorDefaultTransport { t.Error("Expected default transport to be used when none specified") } } app.Shutdown(1 * time.Second) }) } } func TestNewAppChannelBuffering(t *testing.T) { cfg := &Config{ AppName: "test-app", License: testLicenseKey, Enabled: false, Logger: new(testLogger), } app := newApp(config{Config: *cfg}) // Test that initiateShutdown is buffered (size 1) select { case app.initiateShutdown <- 1 * time.Second: // Should succeed without blocking default: t.Error("initiateShutdown channel should be buffered") } // Second send should not block but also shouldn't succeed due to buffer full select { case app.initiateShutdown <- 1 * time.Second: t.Error("Second send should not succeed on buffered channel of size 1") default: // Expected behavior } // Test connectChan buffering run := &appRun{} select { case app.connectChan <- run: // Should succeed without blocking default: t.Error("connectChan should be buffered") } // Test collectorErrorChan buffering resp := rpmResponse{} select { case app.collectorErrorChan <- resp: // Should succeed without blocking default: t.Error("collectorErrorChan should be buffered") } } func TestShouldUseTraceObserver(t *testing.T) { testCases := []struct { name string traceObserverURLSet bool spanEventsEnabled bool distributedEnabled bool expect bool }{ { name: "all required fields enabled", traceObserverURLSet: true, spanEventsEnabled: true, distributedEnabled: true, expect: true, }, { name: "missing traceObserverURL", traceObserverURLSet: false, spanEventsEnabled: true, distributedEnabled: true, expect: false, }, { name: "spanEvents disabled", traceObserverURLSet: true, spanEventsEnabled: false, distributedEnabled: true, expect: false, }, { name: "distributed tracer disabled", traceObserverURLSet: true, spanEventsEnabled: true, distributedEnabled: false, expect: false, }, { name: "all disabled", traceObserverURLSet: false, spanEventsEnabled: false, distributedEnabled: false, expect: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var url *observerURL if tc.traceObserverURLSet { u := observerURL{host: "localhost"} url = &u } c := config{} c.traceObserverURL = url c.SpanEvents.Enabled = tc.spanEventsEnabled c.DistributedTracer.Enabled = tc.distributedEnabled got := shouldUseTraceObserver(c) if got != tc.expect { t.Errorf("shouldUseTraceObserver(%+v) = %v, want %v", c, got, tc.expect) } }) } } func TestHarvestTesting(t *testing.T) { testCases := []struct { name string replyFn func(*internal.ConnectReply) expectGUID string }{ { name: "replyfn sets EntityGUID", replyFn: func(reply *internal.ConnectReply) { reply.EntityGUID = "customGUID" }, expectGUID: "customGUID", }, { name: "replyfn is nil, default EntityGUID", replyFn: nil, expectGUID: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { app := newApp(config{Config: Config{ AppName: "test-app", License: testLicenseKey, Logger: new(testLogger), }}) app.HarvestTesting(tc.replyFn) app.Shutdown(10 * time.Second) if app.testHarvest == nil { t.Fatal("expected testHarvest to be initialized") } if app.placeholderRun == nil { t.Fatal("expected placeholderRun to be initialized") } gotGUID := app.placeholderRun.Reply.EntityGUID if gotGUID != tc.expectGUID { t.Errorf("expected EntityGUID %q, got %q", tc.expectGUID, gotGUID) } }) } } func TestNilAppReturnsNil(t *testing.T) { var app *app txn := app.StartTransaction("test-txn") if txn != nil { t.Errorf("Expected StartTransaction on nil app to return nil, got: %#v", txn) } err := app.RecordCustomEvent("TestEvent", map[string]interface{}{}) if err != nil { t.Errorf("Expected RecordCustomEvent on nil app to return nil, got: %#v", txn) } err = app.RecordCustomMetric("TestMetric", 1.0) if err != nil { t.Errorf("Expected RecordCustomMetric on nil app to return nil, got: %#v", txn) } } func TestRecordCustomEventUnlimitedSizeTypes(t *testing.T) { eventTypes := []string{ "LlmEmbedding", "LlmChatCompletionSummary", "LlmChatCompletionMessage", } params := map[string]interface{}{"foo": "bar"} for _, et := range eventTypes { t.Run(et, func(t *testing.T) { app := newTestApp(nil) err := app.app.RecordCustomEvent(et, params) if err != nil { t.Errorf("expected nil error for eventType %s, got %v", et, err) } app.Shutdown(10 * time.Second) // Validate that the event was recorded with the correct type and params app.ExpectCustomEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": et, "timestamp": internal.MatchAnything, }, UserAttributes: params, }}) }) } } func TestRecordLog(t *testing.T) { testCases := []struct { name string logData LogData wantLog internal.WantLog }{ { name: "basic log event", logData: LogData{ Severity: "Debug", Message: "Test Message", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "Debug", Message: "Test Message", }, }, { name: "info log event", logData: LogData{ Severity: "Info", Message: "Info Message", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "Info", Message: "Info Message", }, }, { name: "warn log event", logData: LogData{ Severity: "Warn", Message: "Warn Message", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "Warn", Message: "Warn Message", }, }, { name: "error log event", logData: LogData{ Severity: "Error", Message: "Error Message", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "Error", Message: "Error Message", }, }, { name: "empty message log event", logData: LogData{ Severity: "Info", Message: "", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "Info", Message: "", }, }, { name: "empty severity log event", logData: LogData{ Severity: "", Message: "No Severity", Timestamp: int64(timeToUnixMilliseconds(time.Now())), }, wantLog: internal.WantLog{ Severity: "UNKNOWN", Message: "No Severity", }, }, { name: "future timestamp log event", logData: LogData{ Severity: "Info", Message: "Future Timestamp", Timestamp: int64(timeToUnixMilliseconds(time.Now().Add(24 * time.Hour))), }, wantLog: internal.WantLog{ Severity: "Info", Message: "Future Timestamp", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, configTestAppLogFn, ) // Ensure timestamps match for comparison if tc.logData.Timestamp != 0 { tc.logData.Timestamp = int64(timeToUnixMilliseconds(time.Now())) tc.wantLog.Timestamp = tc.logData.Timestamp } testApp.RecordLog(tc.logData) testApp.ExpectLogEvents(t, []internal.WantLog{ tc.wantLog, }) testApp.Shutdown(1 * time.Second) }) } t.Run("ApplicationLogging disabled returns nil", func(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, func(cfg *Config) { cfg.Enabled = true cfg.ApplicationLogging.Enabled = false }, ) defer testApp.Shutdown(10 * time.Second) logData := LogData{ Severity: "Info", Message: "Should not record", Timestamp: int64(timeToUnixMilliseconds(time.Now())), } err := testApp.app.RecordLog(&logData) if err != nil { return } if err != nil { t.Errorf("Expected nil error when ApplicationLogging is disabled, got: %v", err) } testApp.ExpectLogEvents(t, []internal.WantLog{}) }) } func TestConsumeIDBehavior(t *testing.T) { testCases := []struct { name string id string expectData bool }{ { name: "empty id does not send data", id: "", expectData: false, }, { name: "non-empty id sends data", id: "1234", expectData: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { app := newApp(config{Config: Config{ AppName: "test-app", License: testLicenseKey, Logger: new(testLogger), }}) app.serverless = newServerlessHarvest(logger.ShimLogger{}, serverlessGetenvShim) // Ensure dataChan is initialized if app.dataChan == nil { app.dataChan = make(chan appData, 1) } app.Consume(internal.AgentRunID(tc.id), customMetric{RawInputName: "shouldNotSend", Value: 123}) select { case <-app.dataChan: if !tc.expectData { t.Error("expected no data to be sent to dataChan when id is empty") } default: if tc.expectData { t.Error("expected data to be sent to dataChan when id is not empty") } } app.Shutdown(100 * time.Millisecond) }) } } func TestTransactionExpectLogEventsMergesThreadData(t *testing.T) { testApp := newTestApp(sampleEverythingReplyFn, configTestAppLogFn) app := testApp.app // Create a transaction and manually add a log event to its thread's harvest txn := app.StartTransaction("txn-log-test") logEvent := &logEvent{ severity: "Info", message: "Thread log message", timestamp: int64(timeToUnixMilliseconds(time.Now())), } // Simulate log event in thread's logs txn.thread.StoreLog(logEvent) // Ensure app.testHarvest is empty before ExpectLogEvents if len(app.testHarvest.LogEvents.logs) != 0 { t.Fatalf("Expected app.testHarvest.LogEvents to be empty before merge") } want := []internal.WantLog{ { Severity: "Info", Message: "Thread log message", Timestamp: logEvent.timestamp, }, } txn.ExpectLogEvents(t, want) // After ExpectLogEvents, app.testHarvest should contain the merged log event if len(app.testHarvest.LogEvents.logs) == 0 { t.Errorf("Expected app.testHarvest.LogEvents to contain merged thread log event") } testApp.Shutdown(1 * time.Second) } type mockTraceObserver struct { connected bool restartCalled bool lastRunID internal.AgentRunID lastHeaders map[string]string } func (m *mockTraceObserver) initialConnCompleted() bool { return m.connected } func (m *mockTraceObserver) restart(runID internal.AgentRunID, headers map[string]string) { m.restartCalled = true m.lastRunID = runID m.lastHeaders = headers } func (m *mockTraceObserver) shutdown(time.Duration) error { return nil } func (m *mockTraceObserver) QueueSize() int { return 0 } func (m *mockTraceObserver) Consume(*spanEvent) {} func (m *mockTraceObserver) consumeSpan(*spanEvent) {} func (m *mockTraceObserver) dumpSupportabilityMetrics() map[string]float64 { return nil } const ( sampleAppName = "my app" ) // expectApp combines Application and Expect, for use in validating data in test apps type expectApplication struct { internal.Expect *Application } func (e expectApplication) Error(i ...interface{}) { } func newTestApp(replyfn func(*internal.ConnectReply), cfgFn ...ConfigOption) expectApplication { cfgFn = append(cfgFn, func(cfg *Config) { // Prevent spawning app goroutines in tests. if !cfg.ServerlessMode.Enabled { cfg.Enabled = false } }, ConfigAppName(sampleAppName), ConfigLicense(testLicenseKey), ConfigCodeLevelMetricsEnabled(false), ) app, err := NewApplication(cfgFn...) if nil != err { panic(err) } internal.HarvestTesting(app.Private, replyfn) return expectApplication{ Expect: app.Private.(internal.Expect), Application: app, } } const ( testEntityGUID = "testEntityGUID123" ) var sampleEverythingReplyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = testEntityGUID } var configTestAppLogFn = func(cfg *Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = true cfg.ApplicationLogging.Metrics.Enabled = true } var configTestAppSamplerFn = func(cfg *Config) { cfg.Enabled = true cfg.RuntimeSampler.Enabled = true cfg.ServerlessMode.Enabled = true } type testLoggerWithMethodTracking struct { messages []string fields []map[string]interface{} calledMethods []string } func (tl *testLoggerWithMethodTracking) DebugEnabled() bool { return true } func (tl *testLoggerWithMethodTracking) Error(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) tl.fields = append(tl.fields, fields) tl.calledMethods = append(tl.calledMethods, "Error") } func (tl *testLoggerWithMethodTracking) Warn(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) tl.fields = append(tl.fields, fields) tl.calledMethods = append(tl.calledMethods, "Warn") } func (tl *testLoggerWithMethodTracking) Info(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) tl.fields = append(tl.fields, fields) tl.calledMethods = append(tl.calledMethods, "Info") } func (tl *testLoggerWithMethodTracking) Debug(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) tl.fields = append(tl.fields, fields) tl.calledMethods = append(tl.calledMethods, "Debug") } type testLogger struct { messages []string } func (tl *testLogger) DebugEnabled() bool { return true } func (tl *testLogger) Error(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) } func (tl *testLogger) Warn(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) } func (tl *testLogger) Info(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) } func (tl *testLogger) Debug(msg string, fields map[string]interface{}) { tl.messages = append(tl.messages, msg) } go-agent-3.42.0/v3/newrelic/internal_attributes_test.go000066400000000000000000001015421510742411500231140ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "math" "net/http" "net/url" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestAddAttributeHighSecurity(t *testing.T) { cfgfn := func(cfg *Config) { cfg.HighSecurity = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.AddAttribute(`key`, 1) app.expectSingleLoggedError(t, "unable to add attribute", map[string]interface{}{ "reason": errHighSecurityEnabled.Error(), }) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } func TestAddAttributeSecurityPolicyDisablesParameters(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.CustomParameters.SetEnabled(false) } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.AddAttribute(`key`, 1) app.expectSingleLoggedError(t, "unable to add attribute", map[string]interface{}{ "reason": errSecurityPolicy.Error(), }) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } func TestAddAttributeSecurityPolicyDisablesInclude(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.AttributesInclude.SetEnabled(false) } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.TransactionEvents.Attributes.Include = append(cfg.TransactionEvents.Attributes.Include, AttributeRequestUserAgent) } val := "dont-include-me-in-txn-events" app := testApp(replyfn, cfgfn, t) req := &http.Request{} req.Header = make(http.Header) req.Header.Add("User-Agent", val) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(req) txn.NoticeError(errors.New("hello")) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{}, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "hello", Klass: "*errors.errorString", AgentAttributes: map[string]interface{}{ AttributeRequestUserAgent: val, AttributeRequestUserAgentDeprecated: val, }, UserAttributes: map[string]interface{}{}, }}) } func TestUserAttributeBasics(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.NoticeError(errors.New("zap")) txn.AddAttribute(`int\key`, 1) app.expectNoLoggedErrors(t) txn.AddAttribute(`str\key`, `zip\zap`) app.expectNoLoggedErrors(t) txn.AddAttribute("invalid_value", struct{}{}) app.expectSingleLoggedError(t, "unable to add attribute", map[string]interface{}{ "reason": `attribute 'invalid_value' value of type struct {} is invalid`, }) txn.AddAttribute("nil_value", nil) app.expectSingleLoggedError(t, "unable to add attribute", map[string]interface{}{ "reason": `attribute 'nil_value' value of type is invalid`, }) txn.End() txn.AddAttribute("already_ended", "zap") app.expectSingleLoggedError(t, "unable to add attribute", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) agentAttributes := map[string]interface{}{} userAttributes := map[string]interface{}{`int\key`: 1, `str\key`: `zip\zap`} app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "zap", Klass: "*errors.errorString", AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "*errors.errorString", "error.message": "zap", "transactionName": "OtherTransaction/Go/hello", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", NumSegments: 0, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) } func TestUserAttributeConfiguration(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.TransactionEvents.Attributes.Exclude = []string{"only_errors", "only_txn_traces"} cfg.ErrorCollector.Attributes.Exclude = []string{"only_txn_events", "only_txn_traces"} cfg.TransactionTracer.Attributes.Exclude = []string{"only_txn_events", "only_errors"} cfg.Attributes.Exclude = []string{"completed_excluded"} cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.NoticeError(errors.New("zap")) txn.AddAttribute("only_errors", 1) app.expectNoLoggedErrors(t) txn.AddAttribute("only_txn_events", 2) app.expectNoLoggedErrors(t) txn.AddAttribute("only_txn_traces", 3) app.expectNoLoggedErrors(t) txn.AddAttribute("completed_excluded", 4) app.expectNoLoggedErrors(t) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{"only_txn_events": 2}, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "zap", Klass: "*errors.errorString", AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{"only_errors": 1}, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "*errors.errorString", "error.message": "zap", "transactionName": "OtherTransaction/Go/hello", }, AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{"only_errors": 1}, }}) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", NumSegments: 0, AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{"only_txn_traces": 3}, }}) } var ( // Agent attributes expected in txn events from usualAttributeTestTransaction. agent1 = map[string]interface{}{ AttributeHostDisplayName: `my\host\display\name`, AttributeResponseCode: `404`, AttributeResponseCodeDeprecated: `404`, AttributeResponseContentType: `text/plain; charset=us-ascii`, AttributeResponseContentLength: 345, AttributeRequestMethod: "GET", AttributeRequestAccept: "text/plain", AttributeRequestContentType: "text/html; charset=utf-8", AttributeRequestContentLength: 753, AttributeRequestHost: "my_domain.com", AttributeRequestURI: "/hello", } // Agent attributes expected in errors and traces from usualAttributeTestTransaction. agent2 = mergeAttributes(agent1, map[string]interface{}{ AttributeRequestUserAgent: "Mozilla/5.0", AttributeRequestUserAgentDeprecated: "Mozilla/5.0", AttributeRequestReferer: "http://en.wikipedia.org/zip", }) // User attributes expected from usualAttributeTestTransaction. user1 = map[string]interface{}{ "myStr": "hello", } ) func agentAttributeTestcase(t testing.TB, cfgfn func(cfg *Config), e AttributeExpect) { app := testApp(nil, func(cfg *Config) { cfg.HostDisplayName = `my\host\display\name` cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.DistributedTracer.Enabled = false if nil != cfgfn { cfgfn(cfg) } }, t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) txn.NoticeError(errors.New("zap")) hdr := rw.Header() hdr.Set("Content-Type", `text/plain; charset=us-ascii`) hdr.Set("Content-Length", `345`) rw.WriteHeader(404) txn.AddAttribute("myStr", "hello") txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, AgentAttributes: e.TxnEvent.Agent, UserAttributes: e.TxnEvent.User, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "zap", Klass: "*errors.errorString", AgentAttributes: e.Error.Agent, UserAttributes: e.Error.User, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "*errors.errorString", "error.message": "zap", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: e.Error.Agent, UserAttributes: e.Error.User, }}) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, AgentAttributes: e.TxnTrace.Agent, UserAttributes: e.TxnTrace.User, }}) } type UserAgent struct { User map[string]interface{} Agent map[string]interface{} } type AttributeExpect struct { TxnEvent UserAgent Error UserAgent TxnTrace UserAgent } func TestAgentAttributes(t *testing.T) { agentAttributeTestcase(t, nil, AttributeExpect{ TxnEvent: UserAgent{ Agent: agent1, User: user1}, Error: UserAgent{ Agent: agent2, User: user1}, }) } func TestAttributesDisabled(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.Attributes.Enabled = false cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, Error: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, TxnTrace: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, }) } func TestDefaultResponseCode(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(&http.Request{}) rw.Write([]byte("hello")) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "S", }, AgentAttributes: map[string]interface{}{ AttributeResponseCode: 200, AttributeResponseCodeDeprecated: 200, }, UserAttributes: map[string]interface{}{}, }}) } func TestNoResponseCode(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") txn.SetWebResponse(w) txn.SetWebRequestHTTP(&http.Request{}) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "S", }, AgentAttributes: map[string]interface{}{}, UserAttributes: map[string]interface{}{}, }}) } func TestTxnEventAttributesDisabled(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.TransactionEvents.Attributes.Enabled = false cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, Error: UserAgent{ Agent: agent2, User: user1}, TxnTrace: UserAgent{ Agent: agent2, User: user1}, }) } func TestErrorAttributesDisabled(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.ErrorCollector.Attributes.Enabled = false cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: agent1, User: user1}, Error: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, TxnTrace: UserAgent{ Agent: agent2, User: user1}, }) } func TestTxnTraceAttributesDisabled(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.TransactionTracer.Attributes.Enabled = false cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: agent1, User: user1}, Error: UserAgent{ Agent: agent2, User: user1}, TxnTrace: UserAgent{ Agent: map[string]interface{}{}, User: map[string]interface{}{}}, }) } var ( allAgentAttributeNames = []string{ AttributeResponseCode, AttributeResponseCodeDeprecated, AttributeRequestMethod, AttributeRequestAccept, AttributeRequestContentType, AttributeRequestContentLength, AttributeRequestHost, AttributeRequestURI, AttributeResponseContentType, AttributeResponseContentLength, AttributeHostDisplayName, AttributeRequestUserAgent, AttributeRequestUserAgentDeprecated, AttributeRequestReferer, } ) func TestAgentAttributesExcluded(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.Attributes.Exclude = allAgentAttributeNames cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: map[string]interface{}{}, User: user1}, Error: UserAgent{ Agent: map[string]interface{}{}, User: user1}, TxnTrace: UserAgent{ Agent: map[string]interface{}{}, User: user1}, }) } func TestAgentAttributesExcludedFromErrors(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.ErrorCollector.Attributes.Exclude = allAgentAttributeNames cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: agent1, User: user1}, Error: UserAgent{ Agent: map[string]interface{}{}, User: user1}, TxnTrace: UserAgent{ Agent: agent2, User: user1}, }) } func TestAgentAttributesExcludedFromTxnEvents(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.TransactionEvents.Attributes.Exclude = allAgentAttributeNames cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: map[string]interface{}{}, User: user1}, Error: UserAgent{ Agent: agent2, User: user1}, TxnTrace: UserAgent{ Agent: agent2, User: user1}, }) } func TestAgentAttributesExcludedFromTxnTraces(t *testing.T) { agentAttributeTestcase(t, func(cfg *Config) { cfg.TransactionTracer.Attributes.Exclude = allAgentAttributeNames cfg.DistributedTracer.Enabled = false }, AttributeExpect{ TxnEvent: UserAgent{ Agent: agent1, User: user1}, Error: UserAgent{ Agent: agent2, User: user1}, TxnTrace: UserAgent{ Agent: map[string]interface{}{}, User: user1}, }) } func TestRequestURIPresent(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") u, err := url.Parse("/hello?remove=me") if nil != err { t.Error(err) } txn.SetWebRequest(WebRequest{URL: u}) txn.NoticeError(errors.New("zap")) txn.End() agentAttributes := map[string]interface{}{"request.uri": "/hello"} userAttributes := map[string]interface{}{} app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "zap", Klass: "*errors.errorString", AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "*errors.errorString", "error.message": "zap", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) } func TestRequestURIExcluded(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.DistributedTracer.Enabled = false cfg.Attributes.Exclude = append(cfg.Attributes.Exclude, AttributeRequestURI) } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") u, err := url.Parse("/hello?remove=me") if nil != err { t.Error(err) } txn.SetWebRequest(WebRequest{URL: u}) txn.NoticeError(errors.New("zap")) txn.End() agentAttributes := map[string]interface{}{} userAttributes := map[string]interface{}{} app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "zap", Klass: "*errors.errorString", AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "*errors.errorString", "error.message": "zap", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, AgentAttributes: agentAttributes, UserAttributes: userAttributes, }}) } func TestMessageAttributes(t *testing.T) { // test that adding message attributes as agent attributes filters them, // but as user attributes does not filter them. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello1") txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeMessageRoutingKey, "myRoutingKey", nil) txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeMessageExchangeType, "myExchangeType", nil) txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeMessageCorrelationID, "myCorrelationID", nil) txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeMessageQueueName, "myQueueName", nil) txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeMessageReplyTo, "myReplyTo", nil) txn.End() txn = app.StartTransaction("hello2") txn.AddAttribute(AttributeMessageRoutingKey, "myRoutingKey") txn.AddAttribute(AttributeMessageExchangeType, "myExchangeType") txn.AddAttribute(AttributeMessageCorrelationID, "myCorrelationID") txn.AddAttribute(AttributeMessageQueueName, "myQueueName") txn.AddAttribute(AttributeMessageReplyTo, "myReplyTo") txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "message.queueName": "myQueueName", "message.routingKey": "myRoutingKey", }, Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello1", }, }, { UserAttributes: map[string]interface{}{ "message.queueName": "myQueueName", "message.routingKey": "myRoutingKey", "message.exchangeType": "myExchangeType", "message.replyTo": "myReplyTo", "message.correlationId": "myCorrelationID", }, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello2", }, }, }) } func TestAddSpanAttr_BasicSegment_AllTypes(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("txn") sg := txn.StartSegment("SegmentName") sg.AddAttribute("attr-string", "this is a string") sg.AddAttribute("attr-float-32", float32(1.5)) sg.AddAttribute("attr-float-64", float64(1.5)) sg.AddAttribute("attr-int", 2) sg.AddAttribute("attr-int-8", int8(3)) sg.AddAttribute("attr-int-16", int16(4)) sg.AddAttribute("attr-int-32", int32(5)) sg.AddAttribute("attr-int-64", int64(6)) sg.AddAttribute("attr-uint", uint(7)) sg.AddAttribute("attr-uint-8", uint8(8)) sg.AddAttribute("attr-uint-16", uint16(9)) sg.AddAttribute("attr-uint-32", uint32(10)) sg.AddAttribute("attr-uint-64", uint64(11)) sg.AddAttribute("attr-uint-ptr", uintptr(12)) sg.AddAttribute("attr-bool", true) sg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/SegmentName", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", }, UserAttributes: map[string]interface{}{ "attr-string": "this is a string", "attr-float-32": 1.5, "attr-float-64": 1.5, "attr-int": 2, "attr-int-8": 3, "attr-int-16": 4, "attr-int-32": 5, "attr-int-64": 6, "attr-uint": 7, "attr-uint-8": 8, "attr-uint-16": 9, "attr-uint-32": 10, "attr-uint-64": 11, "attr-uint-ptr": 12, "attr-bool": true, }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttr_DatastoreSegment(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("txn") ds := &DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: "MySQL", Collection: "users_table", Operation: "SELECT", } ds.AddAttribute("attr-string", "this is a string") ds.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Datastore/statement/MySQL/users_table/SELECT", "sampled": true, "category": "datastore", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", "span.kind": "client", "component": "MySQL", }, UserAttributes: map[string]interface{}{ "attr-string": "this is a string", }, AgentAttributes: map[string]interface{}{ "db.statement": "'SELECT' on 'users_table' using 'MySQL'", "db.collection": "users_table", }, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttr_MessageProducerSegment(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("txn") seg := &MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: "Exchange", DestinationName: "myExchange", } seg.AddAttribute("attr-string", "this is a string") seg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "MessageBroker/RabbitMQ/Exchange/Produce/Named/myExchange", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", }, UserAttributes: map[string]interface{}{ "attr-string": "this is a string", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttr_ExternalSegment(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("txn") seg := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://www.example.com", } seg.AddAttribute("attr-string", "this is a string") seg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "External/www.example.com/http", "sampled": true, "category": "http", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{ "attr-string": "this is a string", }, AgentAttributes: map[string]interface{}{ "http.url": "http://www.example.com", }, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttr_SpanEventsDisabled_TxnTracesNoAttrs(t *testing.T) { app := testApp(distributedTracingReplyFields, func(c *Config) { enableBetterCAT(c) c.SpanEvents.Enabled = false c.TransactionTracer.Threshold.IsApdexFailing = false c.TransactionTracer.Threshold.Duration = 0 }, t) txn := app.StartTransaction("txn") sg := txn.StartSegment("SegmentName") sg.AddAttribute("attr-string", "this is a string") sg.End() txn.End() app.ExpectSpanEvents(t, nil) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/txn", NumSegments: 0, // Ensure the custom attrs weren't added to the txn trace UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }}) } func TestAddSpanAttr_SpanEventsEnabled_TxnTracesNoAttrs(t *testing.T) { app := testApp(distributedTracingReplyFields, func(c *Config) { enableBetterCAT(c) c.SpanEvents.Enabled = true c.TransactionTracer.Threshold.IsApdexFailing = false c.TransactionTracer.Threshold.Duration = 0 }, t) txn := app.StartTransaction("txn") sg := txn.StartSegment("SegmentName") sg.AddAttribute("attr-string", "this is a string") sg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/SegmentName", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", }, UserAttributes: map[string]interface{}{ "attr-string": "this is a string", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/txn", NumSegments: 0, // Ensure the custom attrs weren't added to the txn trace UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }}) } func TestAddSpanAttr_NilSegment(t *testing.T) { // Ensure no panics with a nil segment var sg Segment sg.AddAttribute("attr-string", "this is a string") sg.End() } func TestAddSpanAttr_ValidatedValues(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("txn") sg := txn.StartSegment("SegmentName") sg.AddAttribute("attr-float-32-inf", float32(math.Inf(1))) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute 'attr-float-32-inf' of type float contains an invalid value: +Inf", }) sg.AddAttribute("attr-float-32-nan", float32(math.NaN())) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute 'attr-float-32-nan' of type float contains an invalid value: NaN", }) sg.AddAttribute("attr-float-64-inf", float64(math.Inf(1))) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute 'attr-float-64-inf' of type float contains an invalid value: +Inf", }) sg.AddAttribute("attr-float-64-nan", float64(math.NaN())) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute 'attr-float-64-nan' of type float contains an invalid value: NaN", }) longString := "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789001234567890123456789012345678900123456789012345678901234567890abcdefghijklmnopqrstuvwxyz" sg.AddAttribute(longString, "some-string") app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute key '01234567890123456789012345678901...' exceeds length limit 255", }) sg.AddAttribute("attr-struct", struct{ name string }{name: "invalid struct value"}) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": "attribute 'attr-struct' value of type struct { name string } is invalid", }) sg.AddAttribute("attr-with-long-value", longString) app.expectNoLoggedErrors(t) sg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/SegmentName", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentId": "4981855ad8681d0d", }, // Only the truncated long value should make it through validation. UserAttributes: map[string]interface{}{ "attr-with-long-value": "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789001234567890123", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "transaction.name": "OtherTransaction/Go/txn", "name": "OtherTransaction/Go/txn", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4981855ad8681d0d", "transactionId": "52fdfc072182654f", "nr.entryPoint": true, "traceId": "52fdfc072182654f163f5f0f9a621d72", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } go-agent-3.42.0/v3/newrelic/internal_benchmark_test.go000066400000000000000000000111411510742411500226530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" ) var ( sampleLicense = "0123456789012345678901234567890123456789" ) // BenchmarkMuxWithoutNewRelic acts as a control against the other mux // benchmarks. func BenchmarkMuxWithoutNewRelic(b *testing.B) { mux := http.NewServeMux() mux.HandleFunc(helloPath, handler) w := newCompatibleResponseRecorder() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { mux.ServeHTTP(w, helloRequest) } } // BenchmarkMuxWithNewRelic shows the approximate overhead of instrumenting a // request. The numbers here are approximate since this is a test app: rather // than putting the transaction into a channel to be processed by another // goroutine, the transaction is merged directly into a harvest. func BenchmarkMuxWithNewRelic(b *testing.B) { app := testApp(nil, nil, b) mux := http.NewServeMux() mux.HandleFunc(WrapHandleFunc(app.Application, helloPath, handler)) w := newCompatibleResponseRecorder() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { mux.ServeHTTP(w, helloRequest) } } // BenchmarkTraceSegmentWithDefer shows the overhead of instrumenting a segment // using defer. This and BenchmarkTraceSegmentNoDefer are extremely important: // Timing functions and blocks of code should have minimal cost. func BenchmarkTraceSegmentWithDefer(b *testing.B) { app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) } txn := app.StartTransaction("my txn") fn := func() { defer txn.StartSegment("alpha").End() } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { fn() } } func BenchmarkTraceSegmentNoDefer(b *testing.B) { app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) } txn := app.StartTransaction("my txn") fn := func() { s := txn.StartSegment("alpha") s.End() } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { fn() } } func BenchmarkTraceSegmentZeroSegmentThreshold(b *testing.B) { app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 }, ) if nil != err { b.Fatal(err) } txn := app.StartTransaction("my txn") fn := func() { s := txn.StartSegment("alpha") s.End() } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { fn() } } func BenchmarkDatastoreSegment(b *testing.B) { app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) } txn := app.StartTransaction("my txn") fn := func(txn *Transaction) { ds := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "Select", } defer ds.End() } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { fn(txn) } } func BenchmarkExternalSegment(b *testing.B) { app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) } txn := app.StartTransaction("my txn") fn := func(txn *Transaction) { es := &ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com/", } defer es.End() } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { fn(txn) } } func BenchmarkTxnWithSegment(b *testing.B) { app := testApp(nil, nil, b) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { txn := app.StartTransaction("my txn") txn.StartSegment("myFunction").End() txn.End() } } func BenchmarkTxnWithDatastore(b *testing.B) { app := testApp(nil, nil, b) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { txn := app.StartTransaction("my txn") ds := &DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "Select", } ds.End() txn.End() } } func BenchmarkTxnWithExternal(b *testing.B) { app := testApp(nil, nil, b) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { txn := app.StartTransaction("my txn") es := &ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com", } es.End() txn.End() } } go-agent-3.42.0/v3/newrelic/internal_browser_test.go000066400000000000000000000125661510742411500224200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "reflect" "testing" "github.com/newrelic/go-agent/v3/internal" ) func browserReplyFields(reply *internal.ConnectReply) { reply.AgentLoader = "loader" reply.Beacon = "beacon" reply.BrowserKey = "key" reply.AppID = "app" reply.ErrorBeacon = "error" reply.JSAgentFile = "agent" } func TestBrowserTimingHeaderSuccess(t *testing.T) { includeAttributes := func(cfg *Config) { cfg.BrowserMonitoring.Attributes.Enabled = true cfg.BrowserMonitoring.Attributes.Include = []string{AttributeResponseCode} } app := testApp(browserReplyFields, includeAttributes, t) txn := app.StartTransaction("hello") rw := txn.SetWebResponse(nil) rw.WriteHeader(200) txn.AddAttribute("zip", "zap") hdr := txn.BrowserTimingHeader() app.expectNoLoggedErrors(t) encodingKey := browserEncodingKey(testLicenseKey) obfuscatedTxnName, _ := obfuscate([]byte("OtherTransaction/Go/hello"), encodingKey) obfuscatedAttributes, _ := obfuscate([]byte(`{"u":{"zip":"zap"},"a":{"http.statusCode":200}}`), encodingKey) // This is a cheat: we can't deterministically set this, but DeepEqual // doesn't have any ability to say "equal everything except these // fields". hdr.info.QueueTimeMillis = 12 hdr.info.ApplicationTimeMillis = 34 expected := &BrowserTimingHeader{ agentLoader: "loader", info: browserInfo{ Beacon: "beacon", LicenseKey: "key", ApplicationID: "app", TransactionName: obfuscatedTxnName, QueueTimeMillis: 12, ApplicationTimeMillis: 34, ObfuscatedAttributes: obfuscatedAttributes, ErrorBeacon: "error", Agent: "agent", }, } if !reflect.DeepEqual(hdr, expected) { txnName, _ := deobfuscate(hdr.info.TransactionName, encodingKey) attr, _ := deobfuscate(hdr.info.ObfuscatedAttributes, encodingKey) t.Errorf("header did not match: expected %#v; got %#v txnName=%s attr=%s", expected, hdr, string(txnName), string(attr)) } } func TestBrowserTimingHeaderSuccessWithoutAttributes(t *testing.T) { // Test that attributes do not get put in the browser footer by default // configuration. app := testApp(browserReplyFields, nil, t) txn := app.StartTransaction("hello") rw := txn.SetWebResponse(nil) rw.WriteHeader(200) txn.AddAttribute("zip", "zap") hdr := txn.BrowserTimingHeader() app.expectNoLoggedErrors(t) encodingKey := browserEncodingKey(testLicenseKey) obfuscatedTxnName, _ := obfuscate([]byte("OtherTransaction/Go/hello"), encodingKey) obfuscatedAttributes, _ := obfuscate([]byte(`{"u":{},"a":{}}`), encodingKey) // This is a cheat: we can't deterministically set this, but DeepEqual // doesn't have any ability to say "equal everything except these // fields". hdr.info.QueueTimeMillis = 12 hdr.info.ApplicationTimeMillis = 34 expected := &BrowserTimingHeader{ agentLoader: "loader", info: browserInfo{ Beacon: "beacon", LicenseKey: "key", ApplicationID: "app", TransactionName: obfuscatedTxnName, QueueTimeMillis: 12, ApplicationTimeMillis: 34, ObfuscatedAttributes: obfuscatedAttributes, ErrorBeacon: "error", Agent: "agent", }, } if !reflect.DeepEqual(hdr, expected) { txnName, _ := deobfuscate(hdr.info.TransactionName, encodingKey) attr, _ := deobfuscate(hdr.info.ObfuscatedAttributes, encodingKey) t.Errorf("header did not match: expected %#v; got %#v txnName=%s attr=%s", expected, hdr, string(txnName), string(attr)) } } func TestBrowserTimingHeaderDisabled(t *testing.T) { disableBrowser := func(cfg *Config) { cfg.BrowserMonitoring.Enabled = false } app := testApp(browserReplyFields, disableBrowser, t) txn := app.StartTransaction("hello") hdr := txn.BrowserTimingHeader() app.expectSingleLoggedError(t, "unable to create browser timing header", map[string]interface{}{ "reason": errBrowserDisabled.Error(), }) if hdr.WithTags() != nil { t.Error(hdr.WithTags()) } } func TestBrowserTimingHeaderNotConnected(t *testing.T) { app := testApp(nil, nil, t) txn := app.StartTransaction("hello") hdr := txn.BrowserTimingHeader() // No error expected if the app is not yet connected. app.expectNoLoggedErrors(t) if hdr.WithTags() != nil { t.Error(hdr.WithTags()) } } func TestBrowserTimingHeaderAlreadyFinished(t *testing.T) { app := testApp(browserReplyFields, nil, t) txn := app.StartTransaction("hello") txn.End() hdr := txn.BrowserTimingHeader() app.expectSingleLoggedError(t, "unable to create browser timing header", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) if hdr.WithTags() != nil { t.Error(hdr.WithTags()) } } func TestBrowserTimingHeaderTxnIgnored(t *testing.T) { app := testApp(browserReplyFields, nil, t) txn := app.StartTransaction("hello") txn.Ignore() hdr := txn.BrowserTimingHeader() app.expectSingleLoggedError(t, "unable to create browser timing header", map[string]interface{}{ "reason": errTransactionIgnored.Error(), }) if hdr.WithTags() != nil { t.Error(hdr.WithTags()) } } func BenchmarkBrowserTimingHeaderSuccess(b *testing.B) { app := testApp(browserReplyFields, nil, b) txn := app.StartTransaction("hello") b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { hdr := txn.BrowserTimingHeader() if nil == hdr { b.Fatal(hdr) } app.expectNoLoggedErrors(b) hdr.WithTags() } } go-agent-3.42.0/v3/newrelic/internal_context_test.go000066400000000000000000000117431510742411500224150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestWrapHandlerContext(t *testing.T) { // Test that WrapHandleFunc adds the transaction to the request's // context, and that it is accessible through FromContext. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) _, h := WrapHandleFunc(app.Application, "myTxn", func(rw http.ResponseWriter, r *http.Request) { txn := FromContext(r.Context()) segment := txn.StartSegment("mySegment") segment.End() }) req, _ := http.NewRequest("GET", "", nil) h(nil, req) scope := "WebTransaction/Go/GET myTxn" app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/GET myTxn", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/GET myTxn", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/GET myTxn", Scope: "", Forced: false, Data: nil}, {Name: "Custom/mySegment", Scope: "", Forced: false, Data: nil}, {Name: "Custom/mySegment", Scope: scope, Forced: false, Data: nil}, }) } func TestStartExternalSegmentNilTransaction(t *testing.T) { // Test that StartExternalSegment pulls the transaction from the // request's context if it is not explicitly provided. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("myTxn") req, _ := http.NewRequest("GET", "http://example.com", nil) req = RequestWithTransactionContext(req, txn) segment := StartExternalSegment(nil, req) segment.End() txn.End() scope := "OtherTransaction/Go/myTxn" app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: scope, Forced: false, Data: nil}, }) } func TestNewRoundTripperNilTransaction(t *testing.T) { // Test that NewRoundTripper pulls the transaction from the // request's context if it is not explicitly provided. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("myTxn") client := &http.Client{} client.Transport = roundTripperFunc(func(*http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 202, }, nil }) client.Transport = NewRoundTripper(client.Transport) req, _ := http.NewRequest("GET", "http://example.com", nil) req = RequestWithTransactionContext(req, txn) client.Do(req) txn.End() scope := "OtherTransaction/Go/myTxn" app.ExpectMetrics(t, []internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http/GET", Scope: scope, Forced: false, Data: nil}, {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": "http", "component": "http", "name": "External/example.com/http/GET", "parentId": internal.MatchAnything, "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.method": "GET", "http.statusCode": 202, "http.url": "http://example.com", }, }, { Intrinsics: map[string]interface{}{ "category": "generic", "name": "OtherTransaction/Go/myTxn", "transaction.name": "OtherTransaction/Go/myTxn", "nr.entryPoint": true, "sampled": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } go-agent-3.42.0/v3/newrelic/internal_cross_process_test.go000066400000000000000000000155651510742411500236260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" ) var ( crossProcessReplyFn = func(reply *internal.ConnectReply) { reply.EncodingKey = "encoding_key" reply.CrossProcessID = "12345#67890" reply.TrustedAccounts = map[int]struct{}{ 12345: {}, } } catIntrinsics = map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.pathHash": "fa013f2a", "nr.guid": internal.MatchAnything, "nr.referringTransactionGuid": internal.MatchAnything, "nr.referringPathHash": "41c04f7d", "nr.apdexPerfZone": "S", "client_cross_process_id": "12345#67890", "nr.tripId": internal.MatchAnything, } ) func inboundCrossProcessRequestFactory() *http.Request { cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(crossProcessReplyFn, cfgFn, nil) clientTxn := app.StartTransaction("client") req, err := http.NewRequest("GET", "newrelic.com", nil) StartExternalSegment(clientTxn, req) if req.Header.Get(cat.NewRelicIDName) == "" { panic("missing cat header NewRelicIDName: " + req.Header.Get(cat.NewRelicIDName)) } if req.Header.Get(cat.NewRelicTxnName) == "" { panic("missing cat header NewRelicTxnName: " + req.Header.Get(cat.NewRelicTxnName)) } if err != nil { panic(err) } return req } func outboundCrossProcessResponse() http.Header { cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(crossProcessReplyFn, cfgFn, nil) w := httptest.NewRecorder() txn := app.StartTransaction("txn") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw.WriteHeader(200) return w.HeaderMap } func TestCrossProcessWriteHeaderSuccess(t *testing.T) { // Test that the CAT response header is present when the consumer uses // txn.SetWebResponse().WriteHeader. cfgFn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = true } app := testApp(crossProcessReplyFn, cfgFn, t) w := httptest.NewRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw.WriteHeader(200) txn.End() if w.Header().Get(cat.NewRelicAppDataName) == "" { t.Error(w.Header().Get(cat.NewRelicAppDataName)) } app.ExpectMetrics(t, webMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: catIntrinsics, AgentAttributes: map[string]interface{}{ "request.method": "GET", "httpResponseCode": 200, "http.statusCode": 200, "request.uri": "newrelic.com", }, UserAttributes: map[string]interface{}{}, }}) } func TestCrossProcessWriteSuccess(t *testing.T) { // Test that the CAT response header is present when the consumer uses // txn.Write. cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(crossProcessReplyFn, cfgFn, t) w := httptest.NewRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw.Write([]byte("response text")) txn.End() if "" == w.Header().Get(cat.NewRelicAppDataName) { t.Error(w.Header().Get(cat.NewRelicAppDataName)) } app.ExpectMetrics(t, webMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: catIntrinsics, // Do not test attributes here: In Go 1.5 // response.headers.contentType will be not be present. AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } func TestCrossProcessLocallyDisabled(t *testing.T) { // Test that the CAT can be disabled by local configuration. cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(crossProcessReplyFn, cfgFn, t) w := httptest.NewRecorder() txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw := txn.SetWebResponse(w) rw.Write([]byte("response text")) txn.End() if "" != w.Header().Get(cat.NewRelicAppDataName) { t.Error(w.Header().Get(cat.NewRelicAppDataName)) } app.ExpectMetrics(t, webMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "S", }, // Do not test attributes here: In Go 1.5 // response.headers.contentType will be not be present. AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } func TestCrossProcessDisabledByServerSideConfig(t *testing.T) { // Test that the CAT can be disabled by server-side-config. cfgFn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { crossProcessReplyFn(reply) json.Unmarshal([]byte(`{"agent_config":{"cross_application_tracer.enabled":false}}`), reply) } app := testApp(replyfn, cfgFn, t) w := httptest.NewRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw.Write([]byte("response text")) txn.End() if "" != w.Header().Get(cat.NewRelicAppDataName) { t.Error(w.Header().Get(cat.NewRelicAppDataName)) } app.ExpectMetrics(t, webMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "S", }, // Do not test attributes here: In Go 1.5 // response.headers.contentType will be not be present. AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } func TestCrossProcessEnabledByServerSideConfig(t *testing.T) { // Test that the CAT can be enabled by server-side-config. cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { crossProcessReplyFn(reply) json.Unmarshal([]byte(`{"agent_config":{"cross_application_tracer.enabled":true}}`), reply) } app := testApp(replyfn, cfgFn, t) w := httptest.NewRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(inboundCrossProcessRequestFactory()) rw.Write([]byte("response text")) txn.End() if "" == w.Header().Get(cat.NewRelicAppDataName) { t.Error(w.Header().Get(cat.NewRelicAppDataName)) } app.ExpectMetrics(t, webMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: catIntrinsics, // Do not test attributes here: In Go 1.5 // response.headers.contentType will be not be present. AgentAttributes: nil, UserAttributes: map[string]interface{}{}, }}) } go-agent-3.42.0/v3/newrelic/internal_distributed_trace_test.go000066400000000000000000002534411510742411500244340ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "reflect" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func distributedTracingReplyFields(reply *internal.ConnectReply) { reply.AccountID = "123" reply.AppID = "456" reply.PrimaryAppID = "456" reply.TrustedAccounts = map[int]struct{}{ 123: {}, } reply.TrustedAccountKey = "123" reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(1) reply.DistributedTraceTimestampGenerator = func() time.Time { return time.Unix(1577830891, 900000000) } } func distributedTracingReplyFieldsNeedTrustKey(reply *internal.ConnectReply) { reply.AccountID = "123" reply.AppID = "456" reply.PrimaryAppID = "456" reply.TrustedAccounts = map[int]struct{}{ 123: {}, } reply.TrustedAccountKey = "789" } func distributedTracingReplyFieldsSpansDisabled(reply *internal.ConnectReply) { reply.AccountID = "123" reply.AppID = "456" reply.PrimaryAppID = "456" reply.TrustedAccounts = map[int]struct{}{ 123: {}, } reply.TrustedAccountKey = "123" reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(1) reply.CollectSpanEvents = false reply.DistributedTraceTimestampGenerator = func() time.Time { return time.Unix(1577830891, 900000000) } } func getDTHeaders(app *Application) http.Header { hdrs := http.Header{} app.StartTransaction("hello").thread.CreateDistributedTracePayload(hdrs) return hdrs } func headersFromString(s string) http.Header { return map[string][]string{DistributedTraceNewRelicHeader: {s}} } func makeHeaders(t *testing.T) http.Header { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) return hdrs } func enableOldCATDisableBetterCat(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false } func disableCAT(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = false } func enableBetterCAT(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = true } func enableW3COnly(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true } func enableW3COnlySampledDefault(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentSampled = "default" } func enableW3COnlySampledAlwaysOn(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentSampled = "always_on" } func enableW3COnlyNotSampledDefault(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentNotSampled = "default" } func enableW3COnlySampledAlwaysOff(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentSampled = "always_off" } func enableW3COnlyNotSampledAlwaysOn(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentNotSampled = "always_on" } func enableW3COnlyNotSampledAlwaysOff(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.DistributedTracer.ExcludeNewRelicHeader = true cfg.DistributedTracer.Sampler.RemoteParentNotSampled = "always_off" } func disableSpanEvents(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = false } func disableDistributedTracerEnableSpanEvents(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false cfg.SpanEvents.Enabled = true } var ( distributedTracingSuccessMetrics = []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: singleCount}, } ) func TestPayloadConnection(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs := getDTHeaders(app.Application) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, distributedTracingSuccessMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "parent.type": "App", "parent.account": "123", "parent.app": "456", "parent.transportType": "HTTP", "parent.transportDuration": internal.MatchAnything, "parentId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentSpanId": "9566c74d10d1e2c6", "guid": internal.MatchAnything, "sampled": true, "priority": 1.437714, // priority must be >1 when sampled is true }, }}) } func TestAcceptMultiple(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs := getDTHeaders(app.Application) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errAlreadyAccepted.Error(), }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/Multiple", Scope: "", Forced: true, Data: singleCount}, }, distributedTracingSuccessMetrics...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "parent.type": "App", "parent.account": "123", "parent.app": "456", "parent.transportType": "HTTP", "parent.transportDuration": internal.MatchAnything, "parentId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentSpanId": "9566c74d10d1e2c6", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, }, }}) } func TestInsertDistributedTraceHeadersNotConnected(t *testing.T) { // Test that DT headers do not get created if the connect reply does not // contain the necessary fields. app := testApp(nil, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Error(hdrs) } app.expectNoLoggedErrors(t) } func TestAcceptDistributedTraceHeadersNil(t *testing.T) { // Test that AcceptDistributedTraceHeaders does not have issues // accepting nil headers. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, nil) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/Null", Scope: "", Forced: true, Data: nil}, }, backgroundMetricsUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestAcceptDistributedTraceHeadersBetterCatDisabled(t *testing.T) { // Test that AcceptDistributedTraceHeaders only accepts DT headers if DT // is enabled. app := testApp(nil, disableCAT, t) hdrs := makeHeaders(t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errInboundPayloadDTDisabled.Error(), }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, backgroundMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, }}) } func TestPayloadTransactionsDisabled(t *testing.T) { cfgFn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = true cfg.TransactionEvents.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Fatal(hdrs) } txn.End() app.expectNoLoggedErrors(t) } func TestPayloadConnectionEmptyString(t *testing.T) { app := testApp(nil, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString("")) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, backgroundMetricsUnknownCaller) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestCreatePayloadFinished(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.End() hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Fatal(hdrs) } } func TestAcceptPayloadFinished(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs := getDTHeaders(app.Application) txn := app.StartTransaction("hello") txn.End() app.expectNoLoggedErrors(t) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, backgroundMetricsUnknownCaller) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestPayloadAcceptAfterCreate(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs1 := getDTHeaders(app.Application) txn := app.StartTransaction("hello") hdrs2 := http.Header{} txn.InsertDistributedTraceHeaders(hdrs2) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs1) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errOutboundPayloadCreated.Error(), }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: singleCount}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: singleCount}, {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/CreateBeforeAccept", Scope: "", Forced: true, Data: singleCount}, }, backgroundMetricsUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestPayloadFromApplicationEmptyTransportType(t *testing.T) { // A user has two options when it comes to TransportType. They can either use one of the // defined vars, like TransportHTTP, or create their own empty variable. The name field inside of // the TransportType struct is not exported outside of the package so users cannot modify its value. // When they make the attempt, Go reports: // // implicit assignment of unexported field 'name' in newrelic.TransportType literal. // // This test makes sure an empty TransportType resolves to "Unknown" var emptyTransport TransportType app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"123", "id":"id", "tr":"traceID", "ti":1488325987402 } }` txn.AcceptDistributedTraceHeaders(emptyTransport, headersFromString(p)) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/456/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/DistributedTrace/AcceptPayload/Success", Scope: "", Forced: true, Data: singleCount}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "parent.type": "App", "parent.account": "123", "parent.app": "456", "parent.transportType": "Unknown", "parent.transportDuration": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": "traceID", "parentSpanId": "id", "guid": internal.MatchAnything, }, }}) } func TestPayloadFutureVersion(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") p := `{ "v":[100,0], "d":{ "ty":"App", "ap":"456", "ac":"123", "ti":1488325987402 } }` txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "unsupported major version number 100", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/MajorVersion", Scope: "", Forced: true, Data: singleCount}, }, backgroundUnknownCallerWithTransport...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "guid": internal.MatchAnything, }, }}) } func TestPayloadParsingError(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") p := `{ "v":[0,1], "d":[] }` txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "unable to unmarshal payload data: json: cannot unmarshal array into Go value of type newrelic.payload", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: singleCount}, }, backgroundUnknownCallerWithTransport...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "guid": internal.MatchAnything, }, }}) } func TestPayloadFromFuture(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs := http.Header{} traceParent := "00-52fdfc072182654f163f5f0f9a621d72-9566c74d10037c4d-01" traceState := "123@nr=0-0-123-456-9566c74d10037c4d-52fdfc072182654f-1-0.390345-TIME" futureTime := time.Now().Add(1 * time.Hour) timeStr := fmt.Sprintf("%d", timeToUnixMilliseconds(futureTime)) traceState = strings.Replace(traceState, "TIME", timeStr, 1) hdrs.Set(DistributedTraceW3CTraceParentHeader, traceParent) hdrs.Set(DistributedTraceW3CTraceStateHeader, traceState) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, distributedTracingSuccessMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "parent.type": "App", "parent.account": "123", "parent.app": "456", "parent.transportType": "HTTP", "parent.transportDuration": 0, "parentId": "52fdfc072182654f", "traceId": "52fdfc072182654f163f5f0f9a621d72", "parentSpanId": "9566c74d10037c4d", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, }, }}) } func TestPayloadUntrustedAccount(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"321", "id":"id", "tr":"traceID", "ti":1488325987402 } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errTrustedAccountKey.Error(), }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount", Scope: "", Forced: true, Data: singleCount}, {Name: "Supportability/DistributedTrace/AcceptPayload/Success", Scope: "", Forced: true, Data: singleCount}, }, backgroundUnknownCallerWithTransport...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, }, }}) } func TestPayloadMissingVersion(t *testing.T) { // ensures that a complete distributed trace payload without a version fails app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") p := `{ "d":{ "ty":"App", "ap":"456", "ac":"123", "id":"id", "tr":"traceID", "ti":1488325987402 } }` txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "payload is missing Version/v", }) txn.End() app.expectNoLoggedErrors(t) } func TestTrustedAccountKeyPayloadHasKeyAndMatches(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has a "tk" of 123, which matches the trusted_account_key // from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"321", "id":"id", "tr":"traceID", "ti":1488325987402, "tk":"123" } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) } func TestTrustedAccountKeyPayloadHasKeyAndDoesNotMatch(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has a "tk" of 1234, which does not match the // trusted_account_key from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"321", "id":"id", "tr":"traceID", "ti":1488325987402, "tk":"1234" } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errTrustedAccountKey.Error(), }) txn.End() app.expectNoLoggedErrors(t) } func TestTrustedAccountKeyPayloadMissingKeyAndAccountIdMatches(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has no trust key but its account id of 123 matches // trusted_account_key from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"123", "id":"id", "tr":"traceID", "ti":1488325987402 } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) } func TestTrustedAccountKeyPayloadMissingKeyAndAccountIdDoesNotMatch(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has no trust key and its account id of 1234 does not match the // trusted_account_key from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"1234", "id":"id", "tr":"traceID", "ti":1488325987402 } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errTrustedAccountKey.Error(), }) txn.End() app.expectNoLoggedErrors(t) } var ( backgroundUnknownCaller = []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, } backgroundUnknownCallerWithTransport = []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, } ) func TestNilPayload(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, nil) app.expectNoLoggedErrors(t) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/Null", Scope: "", Forced: true, Data: singleCount}, }, backgroundUnknownCaller...)) } func TestNoticeErrorPayload(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.NoticeError(errors.New("oh no")) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Errors/OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }, backgroundUnknownCaller...)) } func TestMissingIDsForSupportabilityMetric(t *testing.T) { p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"123", "tr":"traceID", "ti":1488325987402 } }` app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "payload is missing both guid/id and TransactionId/tx", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestMissingVersionForSupportabilityMetric(t *testing.T) { p := `{ "d":{ "ty":"App", "ap":"456", "ac":"123", "id":"id", "tr":"traceID", "ti":1488325987402 } }` app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "payload is missing Version/v", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestMissingFieldForSupportabilityMetric(t *testing.T) { p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "id":"id", "tr":"traceID", "ti":1488325987402 } }` app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "payload is missing Account/ac", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestParseExceptionSupportabilityMetric(t *testing.T) { p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "id":"id", "tr":"traceID", "ti":1488325987402 } ` app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "unable to unmarshal payload: unexpected end of JSON input", }) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestErrorsByCaller(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := getDTHeaders(app.Application) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn.NoticeError(errors.New("oh no")) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Errors/all", Scope: "", Forced: true, Data: nil}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Errors/OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, }) } func TestCreateDistributedTraceCatDisabled(t *testing.T) { // when distributed tracing is disabled, CreateDistributedTracePayload // should return a value that indicates an empty payload. Examples of // this depend on language but may be nil/null/None or an empty payload // object. app := testApp(distributedTracingReplyFields, disableCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) // empty/shim payload objects return empty strings if len(hdrs) != 0 { t.Log("Non empty result of InsertDistributedTraceHeaders() method:", hdrs) t.Fail() } txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) } func TestCreateDistributedTraceBetterCatDisabled(t *testing.T) { // when distributed tracing is disabled, CreateDistributedTracePayload // should return a value that indicates an empty payload. Examples of // this depend on language but may be nil/null/None or an empty payload // object. app := testApp(distributedTracingReplyFields, enableOldCATDisableBetterCat, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Log("Non empty result of InsertDistributedTraceHeaders() method:", hdrs) t.Fail() } txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, }) } func TestCreateDistributedTraceBetterCatEnabled(t *testing.T) { // When distributed tracing is enabled and the application is connected, // CreateDistributedTracePayload should return a valid payload object app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) == 0 { t.Log("Empty result of InsertDistributedTraceHeaders() method:", hdrs) t.Fail() } txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func isZeroValue(x interface{}) bool { // https://stackoverflow.com/questions/13901819/quick-way-to-detect-empty-values-via-reflection-in-go return nil == x || x == reflect.Zero(reflect.TypeOf(x)).Interface() } func payloadFieldsFromHeaders(t *testing.T, hdrs http.Header) (out struct { Version []int `json:"v"` Data map[string]interface{} `json:"d"` }) { encoded := hdrs.Get(DistributedTraceNewRelicHeader) decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal("unable to bas64 decode tracing header", err) } if err := json.Unmarshal(decoded, &out); nil != err { t.Fatal("unable to unmarshal payload NRText", err) } return } func testPayloadFieldsPresent(t *testing.T, hdrs http.Header, keys ...string) { out := payloadFieldsFromHeaders(t, hdrs) for _, key := range keys { val, ok := out.Data[key] if !ok { t.Fatal("required key missing", key) } if isZeroValue(val) { t.Fatal("value has default value", key, val) } } } func TestCreateDistributedTraceRequiredFields(t *testing.T) { // creates a distributed trace payload and then checks // to ensure the required fields are in place app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) testPayloadFieldsPresent(t, hdrs, "ty", "ac", "ap", "tr", "ti") txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestCreateDistributedTraceTrustKeyAbsent(t *testing.T) { // creates a distributed trace payload and then checks // to ensure the required fields are in place app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) data := payloadFieldsFromHeaders(t, hdrs) if nil != data.Data["tk"] { t.Fatal("unexpected trust key (tk)", hdrs) } txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestCreateDistributedTraceTrustKeyNeeded(t *testing.T) { // creates a distributed trace payload and then checks // to ensure the required fields are in place app := testApp(distributedTracingReplyFieldsNeedTrustKey, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) testPayloadFieldsPresent(t, hdrs, "tk") txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestCreateDistributedTraceAfterAcceptSampledTrue(t *testing.T) { // simulates 1. reading distributed trace payload from non-header external storage // (for queues, other customer integrations); 2. Accpeting that Payload; 3. Creating // a new payload // tests that the required fields, plus priority and sampled are set app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has a "tk" of 123, which matches the trusted_account_key // from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"321", "id":"id", "tr":"traceID", "ti":1488325987402, "tk":"123", "sa":true } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectNoLoggedErrors(t) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) testPayloadFieldsPresent(t, hdrs, "ty", "ac", "ap", "tr", "ti", "pr", "sa") txn.End() app.expectNoLoggedErrors(t) } func TestCreateDistributedTraceAfterAcceptSampledNotSet(t *testing.T) { // simulates 1. reading distributed trace payload from non-header external storage // (for queues, other customer integrations); 2. Accpeting that Payload; 3. Creating // a new payload // tests that the required fields, plus priority and sampled are set. When "sa" // is not set, the payload should pickup on sampled value of the transaction app := testApp(distributedTracingReplyFields, enableBetterCAT, t) // fixture has a "tk" of 123, which matches the trusted_account_key // from distributedTracingReplyFields. p := `{ "v":[0,1], "d":{ "ty":"App", "ap":"456", "ac":"321", "id":"id", "tr":"traceID", "ti":1488325987402, "tk":"123", "pr":0.54343 } }` txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, headersFromString(p)) app.expectNoLoggedErrors(t) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) testPayloadFieldsPresent(t, hdrs, "ty", "ac", "ap", "id", "tr", "ti", "pr", "sa") txn.End() app.expectNoLoggedErrors(t) } type fieldExpectations struct { Exact map[string]interface{} `json:"exact,omitempty"` Expected []string `json:"expected,omitempty"` Unexpected []string `json:"unexpected,omitempty"` } type distributedTraceTestcase struct { TestName string `json:"test_name"` Comment string `json:"comment,omitempty"` TrustedAccountKey string `json:"trusted_account_key"` AccountID string `json:"account_id"` WebTransaction bool `json:"web_transaction"` RaisesException bool `json:"raises_exception"` ForceSampledTrue bool `json:"force_sampled_true"` SpanEventsEnabled bool `json:"span_events_enabled"` MajorVersion int `json:"major_version"` MinorVersion int `json:"minor_version"` TransportType string `json:"transport_type"` InboundPayloads []json.RawMessage `json:"inbound_payloads"` OutboundPayloads []fieldExpectations `json:"outbound_payloads,omitempty"` Intrinsics struct { TargetEvents []string `json:"target_events"` Common *fieldExpectations `json:"common,omitempty"` Transaction *fieldExpectations `json:"Transaction,omitempty"` Span *fieldExpectations `json:"Span,omitempty"` TransactionError *fieldExpectations `json:"TransactionError,omitempty"` UnexpectedEvents []string `json:"unexpected_events,omitempty"` } `json:"intrinsics"` ExpectedMetrics [][2]interface{} `json:"expected_metrics"` } func (fe *fieldExpectations) add(intrinsics map[string]interface{}) { if nil != fe { for k, v := range fe.Exact { intrinsics[k] = v } for _, v := range fe.Expected { intrinsics[v] = internal.MatchAnything } } } func (fe *fieldExpectations) unexpected() []string { if nil != fe { return fe.Unexpected } return nil } // getTransport ensures that our transport names match cross agent test values. func getTransport(transport string) TransportType { switch TransportType(transport) { case TransportHTTP, TransportHTTPS, TransportKafka, TransportJMS, TransportIronMQ, TransportAMQP, TransportQueue, TransportOther: return TransportType(transport) default: return TransportUnknown } } func runDistributedTraceCrossAgentTestcase(tst *testing.T, tc distributedTraceTestcase, extraAsserts func(expectApp, internal.Validator)) { t := extendValidator(tst, "test="+tc.TestName) configCallback := enableBetterCAT if false == tc.SpanEventsEnabled { configCallback = disableSpanEvents } app := testApp(func(reply *internal.ConnectReply) { reply.AccountID = tc.AccountID reply.AppID = "456" reply.PrimaryAppID = "456" reply.TrustedAccountKey = tc.TrustedAccountKey // if cross agent tests ever include logic for sampling // we'll need to revisit this testing sampler reply.SetSampleEverything() }, configCallback, tst) txn := app.StartTransaction("hello") if tc.WebTransaction { txn.SetWebRequestHTTP(nil) } // If the tests wants us to have an error, give 'em an error if tc.RaisesException { txn.NoticeError(errors.New("my error message")) } // If there are no inbound payloads, invoke Accept on an empty inbound payload. if nil == tc.InboundPayloads { txn.AcceptDistributedTraceHeaders(getTransport(tc.TransportType), nil) } for _, value := range tc.InboundPayloads { // Note that the error return value is not tested here because // some of the tests are intentionally errors. txn.AcceptDistributedTraceHeaders(getTransport(tc.TransportType), headersFromString(string(value))) } //call create each time an outbound payload appears in the testcase for _, expect := range tc.OutboundPayloads { hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) actual := hdrs.Get(DistributedTraceNewRelicHeader) assertTestCaseOutboundPayload(expect, t, actual) } txn.End() // create WantMetrics and assert var wantMetrics []internal.WantMetric for _, metric := range tc.ExpectedMetrics { wantMetrics = append(wantMetrics, internal.WantMetric{Name: metric[0].(string), Scope: "", Forced: nil, Data: nil}) } app.ExpectMetricsPresent(t, wantMetrics) // Add extra fields that are not listed in the JSON file so that we can // always do exact intrinsic set match. extraTxnFields := &fieldExpectations{Expected: []string{"name"}} if tc.WebTransaction { extraTxnFields.Expected = append(extraTxnFields.Expected, "nr.apdexPerfZone") } extraSpanFields := &fieldExpectations{ Expected: []string{"name", "transaction.name", "category", "nr.entryPoint"}, } // There is a single test with an error (named "exception"), so these // error expectations can be hard coded. TODO: Move some of these. // fields into the cross agent tests. extraErrorFields := &fieldExpectations{ Expected: []string{"parent.type", "parent.account", "parent.app", "parent.transportType", "error.message", "transactionName", "parent.transportDuration", "error.class", "spanId"}, } for _, value := range tc.Intrinsics.TargetEvents { switch value { case "Transaction": assertTestCaseIntrinsics(t, app.ExpectTxnEvents, tc.Intrinsics.Common, tc.Intrinsics.Transaction, extraTxnFields) case "Span": assertTestCaseIntrinsics(t, app.ExpectSpanEvents, tc.Intrinsics.Common, tc.Intrinsics.Span, extraSpanFields) case "TransactionError": assertTestCaseIntrinsics(t, app.ExpectErrorEvents, tc.Intrinsics.Common, tc.Intrinsics.TransactionError, extraErrorFields) } } extraAsserts(app, t) } func assertTestCaseOutboundPayload(expect fieldExpectations, t internal.Validator, encoded string) { decoded, err := base64.StdEncoding.DecodeString(encoded) if nil != err { t.Error("unable to decode payload header", err) return } type outboundTestcase struct { Version [2]uint `json:"v"` Data map[string]interface{} `json:"d"` } var actualPayload outboundTestcase err = json.Unmarshal([]byte(decoded), &actualPayload) if nil != err { t.Error(err) } // Affirm that the exact values are in the payload. for k, v := range expect.Exact { if k != "v" { field := strings.Split(k, ".")[1] if v != actualPayload.Data[field] { t.Error(fmt.Sprintf("exact outbound payload field mismatch key=%s wanted=%v got=%v", k, v, actualPayload.Data[field])) } } } // Affirm that the expected values are in the actual payload. for _, e := range expect.Expected { field := strings.Split(e, ".")[1] if nil == actualPayload.Data[field] { t.Error(fmt.Sprintf("expected outbound payload field missing key=%s", e)) } } // Affirm that the unexpected values are not in the actual payload. for _, u := range expect.Unexpected { field := strings.Split(u, ".")[1] if nil != actualPayload.Data[field] { t.Error(fmt.Sprintf("unexpected outbound payload field present key=%s", u)) } } } func assertTestCaseIntrinsics(t internal.Validator, expect func(internal.Validator, []internal.WantEvent), fields ...*fieldExpectations) { intrinsics := map[string]interface{}{} for _, f := range fields { f.add(intrinsics) } expect(t, []internal.WantEvent{{Intrinsics: intrinsics}}) } func TestDistributedTraceCrossAgent(t *testing.T) { var tcs []distributedTraceTestcase data, err := crossagent.ReadFile(`distributed_tracing/distributed_tracing.json`) if nil != err { t.Fatal(err) } if err := json.Unmarshal(data, &tcs); nil != err { t.Fatal(err) } // Test that we are correctly parsing all of the testcase fields by // comparing an opaque object from original JSON to an object from JSON // created by our testcases. backToJSON, err := json.Marshal(tcs) if nil != err { t.Fatal(err) } var fromFile []map[string]interface{} var fromMarshalled []map[string]interface{} if err := json.Unmarshal(data, &fromFile); nil != err { t.Fatal(err) } if err := json.Unmarshal(backToJSON, &fromMarshalled); nil != err { t.Fatal(err) } if !reflect.DeepEqual(fromFile, fromMarshalled) { t.Error(internal.CompactJSONString(string(data)), "\n", internal.CompactJSONString(string(backToJSON))) } // Iterate over all cross-agent tests for _, tc := range tcs { extraAsserts := func(app expectApp, t internal.Validator) {} if "spans_disabled_in_child" == tc.TestName { // if span events are disabled but distributed tracing is enabled, then // we expect there are zero span events extraAsserts = func(app expectApp, t internal.Validator) { app.ExpectSpanEvents(t, nil) } } runDistributedTraceCrossAgentTestcase(t, tc, extraAsserts) } } func TestDistributedTraceDisabledSpanEventsEnabled(t *testing.T) { app := testApp(distributedTracingReplyFields, disableDistributedTracerEnableSpanEvents, t) hdrs := makeHeaders(t) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": errInboundPayloadDTDisabled.Error(), }) txn.End() app.expectNoLoggedErrors(t) // ensure no span events created app.ExpectSpanEvents(t, nil) } func TestCreatePayloadAppNotConnected(t *testing.T) { // Test that an app which isn't connected does not create distributed // trace payloads. app := testApp(nil, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Error(hdrs) } } func TestCreatePayloadReplyMissingTrustKey(t *testing.T) { // Test that an app whose reply is missing the trust key does not create // distributed trace payloads. app := testApp(func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.TrustedAccountKey = "" }, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) != 0 { t.Error(hdrs) } } func TestAcceptPayloadAppNotConnected(t *testing.T) { // Test that an app which isn't connected does not accept distributed // trace payloads. app := testApp(nil, enableBetterCAT, t) txn := testApp(distributedTracingReplyFields, enableBetterCAT, t). StartTransaction("name") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) == 0 { t.Fatal(hdrs) } txn2 := app.StartTransaction("hello") txn2.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn2.End() app.ExpectMetrics(t, backgroundUnknownCaller) } func TestAcceptPayloadReplyMissingTrustKey(t *testing.T) { // Test that an app whose reply is missing a trust key does not accept // distributed trace payloads. app := testApp(func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.TrustedAccountKey = "" }, enableBetterCAT, t) txn := testApp(distributedTracingReplyFields, enableBetterCAT, t). StartTransaction("name") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) == 0 { t.Fatal(hdrs) } txn2 := app.StartTransaction("hello") txn2.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) txn2.End() app.ExpectMetrics(t, backgroundUnknownCaller) } func verifyHeaders(t *testing.T, actual http.Header, expected http.Header) { if !reflect.DeepEqual(actual, expected) { t.Error("Headers do not match - expected/actual: ", expected, actual) } } func TestW3CTraceHeaders(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersRemoteParentSamplingDefault(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnlySampledDefault, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersRemoteParentNotSamplingDefault(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnlyNotSampledDefault, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-0-0.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersRemoteParentSamplingAlwaysOn(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnlySampledAlwaysOn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-2-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "priority": 2.0, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, }, }) } func TestW3CTraceHeadersRemoteParentSamplingAlwaysOff(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnlySampledAlwaysOff, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-0-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "priority": 0.0, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, }, }) } func TestW3CTraceHeadersRemoteParentNoSamplingAlwaysOn(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnlyNotSampledAlwaysOn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-0-2-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "priority": 2.0, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, }, }) } func TestW3CTraceHeadersRemoteParentNoSamplingAlwaysOff(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnlyNotSampledAlwaysOff, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-0-0-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "priority": 0.0, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, }, }, }) } var acceptAndSendDT = []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/1349956/41346604/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/1349956/41346604/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/1349956/41346604/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/1349956/41346604/HTTP/allOther", Scope: "", Forced: false, Data: nil}, } func TestW3CTraceHeadersNoMatchingNREntry(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Set(DistributedTraceW3CTraceParentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") hdrs.Set(DistributedTraceW3CTraceStateHeader, "99999@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) outgoingHdrs := http.Header{} txn.InsertDistributedTraceHeaders(outgoingHdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900,99999@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277"}, } verifyHeaders(t, outgoingHdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/TraceState/NoNrEntry", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "priority": internal.MatchAnything, "category": "generic", "parentId": "00f067aa0ba902b7", "nr.entryPoint": true, "guid": "9566c74d10d1e2c6", "transactionId": "52fdfc072182654f", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "tracingVendors": "99999@nr", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "parent.transportType": "HTTP", }, }, }) } func TestW3CTraceHeadersRoundTrip(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Set(DistributedTraceW3CTraceParentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") hdrs.Set(DistributedTraceW3CTraceStateHeader, "123@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) outgoingHdrs := http.Header{} txn.InsertDistributedTraceHeaders(outgoingHdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-0.24689-1577830891900"}, } verifyHeaders(t, outgoingHdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, acceptAndSendDT) } func TestW3CTraceHeadersDuplicateTraceState(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Set(DistributedTraceW3CTraceParentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") hdrs.Set(DistributedTraceW3CTraceStateHeader, "123@nr=0-0-1349956-41346604-27ddd2d8890283b4-b28be285632bbc0a-1-0.246890-1569367663277,congo=congosSecondPosition,rojo=rojosFirstPosition,123@nr=0-0-1349956-41346604-aaaaaaaaaaaaaaaa-b28be285632bbc0a-1-0.246890-1569367663277") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) outgoingHdrs := http.Header{} txn.InsertDistributedTraceHeaders(outgoingHdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-0.24689-1577830891900,congo=congosSecondPosition,rojo=rojosFirstPosition"}, } verifyHeaders(t, outgoingHdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, acceptAndSendDT) } func TestW3CTraceHeadersSpansDisabledSampledTrue(t *testing.T) { app := testApp(distributedTracingReplyFieldsSpansDisabled, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456--52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersSpansDisabledSampledFalse(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFieldsSpansDisabled(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456--52fdfc072182654f-0-0.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersSpansDisabledWithTraceState(t *testing.T) { app := testApp(distributedTracingReplyFieldsSpansDisabled, enableW3COnly, t) txn := app.StartTransaction("hello") originalTraceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" originalTraceState := "rojo=00f067aa0ba902b7" incomingHdrs := http.Header{} incomingHdrs.Set(DistributedTraceW3CTraceParentHeader, originalTraceParent) incomingHdrs.Set(DistributedTraceW3CTraceStateHeader, originalTraceState) txn.AcceptDistributedTraceHeaders(TransportHTTP, incomingHdrs) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456--52fdfc072182654f-1-1.437714-1577830891900," + originalTraceState}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/TraceState/NoNrEntry", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestW3CTraceHeadersTxnEventsDisabled(t *testing.T) { cfgfn := func(cfg *Config) { enableW3COnly(cfg) cfg.TransactionEvents.Enabled = false } app := testApp(distributedTracingReplyFields, cfgfn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6--1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersTxnAndSpanEventsDisabledSampledTrue(t *testing.T) { cfgfn := func(cfg *Config) { enableW3COnly(cfg) cfg.TransactionEvents.Enabled = false } app := testApp(distributedTracingReplyFieldsSpansDisabled, cfgfn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456---1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersTxnAndSpanEventsDisabledSampledFalse(t *testing.T) { cfgfn := func(cfg *Config) { enableW3COnly(cfg) cfg.TransactionEvents.Enabled = false } app := testApp(distributedTracingReplyFieldsSpansDisabled, cfgfn, t) txn := app.StartTransaction("hello") hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456---1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceHeadersNoTraceState(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") originalTraceParent := "00-12345678901234567890123456789012-1234567890123456-01" incomingHdrs := http.Header{} incomingHdrs.Set(DistributedTraceW3CTraceParentHeader, originalTraceParent) txn.AcceptDistributedTraceHeaders(TransportHTTP, incomingHdrs) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-12345678901234567890123456789012-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectNoLoggedErrors(t) } // Based on test_traceparent_trace_id_all_zero in // https://github.com/w3c/trace-context/blob/3d02cfc15778ef850df9bc4e9d2740a4a2627fd5/test/test.py func TestW3CTraceHeadersInvalidTraceID(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") originalTraceParent := "00-00000000000000000000000000000000-1234567890123456-01" incomingHdrs := http.Header{} incomingHdrs.Set(DistributedTraceW3CTraceParentHeader, originalTraceParent) txn.AcceptDistributedTraceHeaders(TransportHTTP, incomingHdrs) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "invalid TraceParent trace ID", }) } // Based on test_traceparent_parent_id_all_zero in // https://github.com/w3c/trace-context/blob/3d02cfc15778ef850df9bc4e9d2740a4a2627fd5/test/test.py func TestW3CTraceHeadersInvalidParentID(t *testing.T) { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") originalTraceParent := "00-12345678901234567890123456789012-0000000000000000-01" incomingHdrs := http.Header{} incomingHdrs.Set(DistributedTraceW3CTraceParentHeader, originalTraceParent) txn.AcceptDistributedTraceHeaders(TransportHTTP, incomingHdrs) hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) expected := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-01"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-1-1.437714-1577830891900"}, } verifyHeaders(t, hdrs, expected) txn.End() app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": "invalid TraceParent parent ID", }) } // Based on test_traceparent_version_0x00, test_traceparent_version_0xcc, test_traceparent_version_0xff in // https://github.com/w3c/trace-context/blob/3d02cfc15778ef850df9bc4e9d2740a4a2627fd5/test/test.py func TestW3CTraceHeadersFutureVersion(t *testing.T) { cases := map[string]string{ "00-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like": "invalid TraceParent flags for this version", "cc-12345678901234567890123456789012-1234567890123456-01": "", "cc-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like": "", "cc-12345678901234567890123456789012-1234567890123456-01.what-the-future-will-be-like": "invalid number of TraceParent entries", "ff-12345678901234567890123456789012-1234567890123456-01": "invalid TraceParent flags for this version", } for testCase, failureMessage := range cases { app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") originalTraceParent := testCase incomingHdrs := http.Header{} incomingHdrs.Set(DistributedTraceW3CTraceParentHeader, originalTraceParent) txn.AcceptDistributedTraceHeaders(TransportHTTP, incomingHdrs) outgoingHdrs := http.Header{} txn.InsertDistributedTraceHeaders(outgoingHdrs) if len(outgoingHdrs) != 2 { t.Log("Not all headers present:", outgoingHdrs) t.Fail() } expected := "00-12345678901234567890123456789012-9566c74d10d1e2c6-01" if failureMessage != "" { if outgoingHdrs.Get(DistributedTraceW3CTraceParentHeader) == expected { t.Errorf("Invalid TraceParent header resulting from %s", testCase) } } else { if outgoingHdrs.Get(DistributedTraceW3CTraceParentHeader) != expected { t.Errorf("Invalid TraceParent header resulting from %s", testCase) } } txn.End() if failureMessage != "" { app.expectSingleLoggedError(t, "unable to accept trace payload", map[string]interface{}{ "reason": failureMessage, }) } else { app.expectNoLoggedErrors(t) } } } func TestW3CTraceParentWithoutTraceContext(t *testing.T) { traceparent := "00-050c91b77efca9b0ef38b30c182355ce-560ccffb087d1906-01" app := testApp(distributedTracingReplyFields, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Set(DistributedTraceW3CTraceParentHeader, traceparent) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "traceId": "050c91b77efca9b0ef38b30c182355ce", "parentSpanId": "560ccffb087d1906", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "parent.transportType": "HTTP", }, }}) } func TestDistributedTraceInteroperabilityErrorFallbacks(t *testing.T) { // Test what happens in varying cases when both w3c and newrelic headers // are found // parent.type = "App" // parentSpanId = "5f474d64b9cc9b2a" // traceId = "3221bf09aa0bcf0d3221bf09aa0bcf0d" newrelicHdr := `{ "v": [0,1], "d": { "ty": "App", "ac": "123", "ap": "51424", "id": "5f474d64b9cc9b2a", "tr": "3221bf09aa0bcf0d3221bf09aa0bcf0d", "pr": 0.1234, "sa": true, "ti": 1482959525577, "tx": "27856f70d3d314b7" } }` // parentSpanId = "560ccffb087d1906" // traceId = "050c91b77efca9b0ef38b30c182355ce" traceparentHdr := "00-050c91b77efca9b0ef38b30c182355ce-560ccffb087d1906-01" // parent.type = "Browser" tracestateHdr := "123@nr=0-1-123-456-1234567890123456-6543210987654321-0-0.24689-0" testcases := []struct { name string traceparent string tracestate string newrelic string expIntrinsics map[string]interface{} }{ { name: "w3c present, newrelic absent, failure to parse traceparent", traceparent: "garbage", tracestate: tracestateHdr, newrelic: "", expIntrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "name": internal.MatchAnything, "traceId": "52fdfc072182654f163f5f0f9a621d72", // randomly generated }, }, { name: "w3c present, newrelic absent, failure to parse tracestate", traceparent: traceparentHdr, tracestate: "123@nr=garbage", newrelic: "", expIntrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "name": internal.MatchAnything, "parent.transportType": internal.MatchAnything, "parentSpanId": "560ccffb087d1906", // from traceparent header "traceId": "050c91b77efca9b0ef38b30c182355ce", // from traceparent header }, }, { name: "w3c present, newrelic present, failure to parse traceparent", traceparent: "garbage", tracestate: tracestateHdr, newrelic: newrelicHdr, expIntrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "name": internal.MatchAnything, "traceId": "52fdfc072182654f163f5f0f9a621d72", // randomly generated }, }, { name: "w3c present, newrelic present, failure to parse tracestate", traceparent: traceparentHdr, tracestate: "123@nr=garbage", newrelic: newrelicHdr, expIntrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "name": internal.MatchAnything, "parent.transportType": internal.MatchAnything, "parentSpanId": "560ccffb087d1906", // from traceparent header "traceId": "050c91b77efca9b0ef38b30c182355ce", // from traceparent header }, }, { name: "w3c present, newrelic present", traceparent: traceparentHdr, tracestate: tracestateHdr, newrelic: newrelicHdr, expIntrinsics: map[string]interface{}{ "parent.app": internal.MatchAnything, "parent.transportDuration": internal.MatchAnything, "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "parent.account": internal.MatchAnything, "parentId": internal.MatchAnything, "name": internal.MatchAnything, "parent.transportType": internal.MatchAnything, "parent.type": "Browser", // from tracestate header "parentSpanId": "560ccffb087d1906", // from traceparent header "traceId": "050c91b77efca9b0ef38b30c182355ce", // from traceparent header }, }, { name: "w3c absent, newrelic present", traceparent: "", tracestate: "", newrelic: newrelicHdr, expIntrinsics: map[string]interface{}{ "parent.app": internal.MatchAnything, "parent.transportDuration": internal.MatchAnything, "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "parent.account": internal.MatchAnything, "parentId": internal.MatchAnything, "name": internal.MatchAnything, "parent.transportType": internal.MatchAnything, "parent.type": "App", // from newrelic header "parentSpanId": "5f474d64b9cc9b2a", // from newrelic header "traceId": "3221bf09aa0bcf0d3221bf09aa0bcf0d", // from newrelic header }, }, { name: "w3c absent, newrelic absent", traceparent: "", tracestate: "", newrelic: "", expIntrinsics: map[string]interface{}{ "guid": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "name": internal.MatchAnything, "traceId": "52fdfc072182654f163f5f0f9a621d72", // randomly generated }, }, } addHdr := func(hdrs http.Header, key, val string) { if val != "" { hdrs.Add(key, val) } } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} addHdr(hdrs, DistributedTraceW3CTraceParentHeader, tc.traceparent) addHdr(hdrs, DistributedTraceW3CTraceStateHeader, tc.tracestate) addHdr(hdrs, DistributedTraceNewRelicHeader, tc.newrelic) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: tc.expIntrinsics, }}) }) } } func TestW3CTraceStateMultipleHeaders(t *testing.T) { traceparent := "00-050c91b77efca9b0ef38b30c182355ce-560ccffb087d1906-01" nrstatekey := "123@nr=0-0-123-456-1234567890123456-6543210987654321-1-0.24689-0" testcases := []struct { firstheader string secondheader string }{ {firstheader: "a=1,b=2", secondheader: nrstatekey}, {firstheader: "a=1", secondheader: "b=2," + nrstatekey}, {firstheader: "a=1", secondheader: nrstatekey + ",b=2"}, } for _, tc := range testcases { t.Run(tc.firstheader+"_"+tc.secondheader, func(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Add(DistributedTraceW3CTraceParentHeader, traceparent) hdrs.Add(DistributedTraceW3CTraceStateHeader, tc.firstheader) hdrs.Add(DistributedTraceW3CTraceStateHeader, tc.secondheader) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.expectNoLoggedErrors(t) app.ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "category": "generic", "guid": "9566c74d10d1e2c6", "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, "parentId": "560ccffb087d1906", "priority": internal.MatchAnything, "sampled": true, "traceId": "050c91b77efca9b0ef38b30c182355ce", "tracingVendors": "a,b", // ensures both headers read "transactionId": "52fdfc072182654f", "trustedParentId": "1234567890123456", }, }}) }) } } func TestW3CTraceIDLengths(t *testing.T) { // Test that if the agent received an inbound traceId that is less than 32 // characters, the traceId included in an outbound payload must be // left-padded with zeros. If it is too long, we chop off from the left. testcases := []string{ "3221bf09aa0bcf0d", // too short "00000000000000000000000000000000000000000000003221bf09aa0bcf0d", // too long } for _, teststr := range testcases { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") in := http.Header{} in.Add(DistributedTraceNewRelicHeader, fmt.Sprintf(`{ "v": [0,1], "d": { "ty": "App", "ac": "123", "ap": "51424", "id": "5f474d64b9cc9b2a", "tr": "%s", "pr": 0.1234, "sa": true, "ti": 1482959525577, "tx": "27856f70d3d314b7" } }`, teststr)) txn.AcceptDistributedTraceHeaders(TransportHTTP, in) app.expectNoLoggedErrors(t) out := http.Header{} txn.InsertDistributedTraceHeaders(out) traceparent := out.Get(DistributedTraceW3CTraceParentHeader) expected := "00-00000000000000003221bf09aa0bcf0d-9566c74d10d1e2c6-01" if traceparent != expected { t.Errorf("incorrect traceparent header: expect=%s actual=%s", expected, traceparent) } } } func TestW3CTraceNotSampledOutboundHeaders(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := make(http.Header) txn.InsertDistributedTraceHeaders(hdrs) if !reflect.DeepEqual(hdrs, http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=0-0-123-456-9566c74d10d1e2c6-52fdfc072182654f-0-0.437714-1577830891900"}, }) { t.Error(hdrs) } txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Create/Success", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCaller...)) } func TestW3CTraceStateInvalidNrEntry(t *testing.T) { // If the tracestate header has fewer entries (separated by '-') than // expected, make sure the correct Supportability metrics are created replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableW3COnly, t) txn := app.StartTransaction("hello") hdrs := http.Header{ DistributedTraceW3CTraceParentHeader: []string{"00-52fdfc072182654f163f5f0f9a621d72-9566c74d10d1e2c6-00"}, DistributedTraceW3CTraceStateHeader: []string{"123@nr=garbage"}, } txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.expectNoLoggedErrors(t) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: nil}, {Name: "Supportability/TraceContext/TraceState/InvalidNrEntry", Scope: "", Forced: true, Data: nil}, }, backgroundUnknownCallerWithTransport...)) } func TestUpperCaseTraceIDReceived(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.SetSampleNothing() } app := testApp(replyfn, enableBetterCAT, t) txn := app.StartTransaction("hello") originalTraceID := "85D7FA2DD1B66D6C" // Legacy .NET agents may send uppercase trace IDs incoming := payload{ Type: callerTypeApp, App: "123", Account: "456", TransactionID: "1a2b3c", ID: "0f9a8d", TracedID: originalTraceID, TrustedAccountKey: "123", } hdrs := http.Header{ DistributedTraceNewRelicHeader: []string{incoming.NRText()}, } txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) outgoing := http.Header{} txn.InsertDistributedTraceHeaders(outgoing) // Verify the NR header uses the original (short, uppercase) Trace ID p := payloadFieldsFromHeaders(t, outgoing) s, ok := p.Data["tr"].(string) if !ok || s != originalTraceID { t.Error("Invalid NewRelic header trace ID", p.Data) } // Verify that the TraceParent header uses padded and lower-cased Trace ID ts := outgoing.Get(DistributedTraceW3CTraceParentHeader) expected := "0000000000000000" + strings.ToLower(originalTraceID) if ts != "00-"+expected+"-9566c74d10d1e2c6-00" { t.Error("Invalid TraceParent header", ts) } } func TestDTHeadersAddedTwice(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") // make sure outbound headers are not added twice assertHdrs := func(hdrs http.Header) { if h := hdrs[DistributedTraceNewRelicHeader]; len(h) != 1 { t.Errorf("incorrect number of newrelic headers: %#v", h) } if h := hdrs[DistributedTraceW3CTraceParentHeader]; len(h) != 1 { t.Errorf("incorrect number of traceparent headers: %#v", h) } if h := hdrs[DistributedTraceW3CTraceStateHeader]; len(h) != 1 { t.Errorf("incorrect number of tracestate headers: %#v", h) } } // Using StartExternalSegment req, _ := http.NewRequest("GET", "https://www.something.com/path/zip/zap?secret=ssshhh", nil) s := StartExternalSegment(txn, req) s.End() s = StartExternalSegment(txn, req) s.End() app.expectNoLoggedErrors(t) assertHdrs(req.Header) // Using InsertDistributedTraceHeaders hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) txn.InsertDistributedTraceHeaders(hdrs) assertHdrs(hdrs) } func TestW3CHeaderCases(t *testing.T) { traceparent := "00-050c91b77efca9b0ef38b30c182355ce-560ccffb087d1906-01" tracestate := "123@nr=0-0-123-456-1234567890123456-6543210987654321-0-0.24689-0" testcases := []struct { parent string state string }{ {parent: "traceparent", state: "Tracestate"}, {parent: "Traceparent", state: "Tracestate"}, {parent: "TraceParent", state: "Tracestate"}, {parent: "Traceparent", state: "tracestate"}, {parent: "Traceparent", state: "Tracestate"}, {parent: "Traceparent", state: "TraceState"}, } for _, tc := range testcases { t.Run(tc.parent+"-"+tc.state, func(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") hdrs := http.Header{} hdrs.Set(tc.parent, traceparent) hdrs.Set(tc.state, tracestate) txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectMetricsPresent(t, []internal.WantMetric{ // presence of this metric indicates that accepting succeeded {Name: "DurationByCaller/App/123/456/HTTP/all"}, }) }) } } go-agent-3.42.0/v3/newrelic/internal_errors_13_test.go000066400000000000000000000025361510742411500225500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.13 // +build go1.13 package newrelic import ( "fmt" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestNoticedWrappedError(t *testing.T) { gamma := func() error { return Error{ Message: "socket error", Class: "socketError", Attributes: map[string]interface{}{ "zip": "zap", }, } } beta := func() error { return fmt.Errorf("problem in beta: %w", gamma()) } alpha := func() error { return fmt.Errorf("problem in alpha: %w", beta()) } app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(alpha()) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "problem in alpha: problem in beta: socket error", Klass: "socketError", UserAttributes: map[string]interface{}{ "zip": "zap", }, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "socketError", "error.message": "problem in alpha: problem in beta: socket error", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{ "zip": "zap", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } go-agent-3.42.0/v3/newrelic/internal_errors_stacktrace_test.go000066400000000000000000000045051510742411500244470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "runtime" "strings" "testing" ) // The use of runtime.CallersFrames requires Go 1.7+. func topFrameFunction(stack []uintptr) string { var frame runtime.Frame frames := runtime.CallersFrames(stack) if nil != frames { frame, _ = frames.Next() } return frame.Function } type withStackAndCause struct { cause error stack []uintptr } type withStack struct { stack []uintptr } func (e withStackAndCause) Error() string { return e.cause.Error() } func (e withStackAndCause) StackTrace() []uintptr { return e.stack } func (e withStackAndCause) Unwrap() error { return e.cause } func (e withStack) Error() string { return "something went wrong" } func (e withStack) StackTrace() []uintptr { return e.stack } func generateStack() []uintptr { skip := 2 // skip runtime.Callers and this function. callers := make([]uintptr, 20) written := runtime.Callers(skip, callers) return callers[:written] } func alpha() []uintptr { return generateStack() } func beta() []uintptr { return generateStack() } func TestStackTrace(t *testing.T) { // First choice is any StackTrace() of the immediate error. // Second choice is any StackTrace() of the error's cause. // Final choice is stack trace of the current location. getStackTraceFrame := "github.com/newrelic/go-agent/v3/newrelic.getStackTrace" testcases := []struct { Error error ExpectTopFrame string }{ {Error: basicError{}, ExpectTopFrame: getStackTraceFrame}, {Error: withStack{stack: alpha()}, ExpectTopFrame: "alpha"}, {Error: withStack{stack: nil}, ExpectTopFrame: getStackTraceFrame}, {Error: withStackAndCause{stack: alpha(), cause: basicError{}}, ExpectTopFrame: "alpha"}, {Error: withStackAndCause{stack: nil, cause: withStack{stack: beta()}}, ExpectTopFrame: "beta"}, {Error: withStackAndCause{stack: nil, cause: withStack{stack: nil}}, ExpectTopFrame: getStackTraceFrame}, } for idx, tc := range testcases { data, err := errDataFromError(tc.Error, false) if err != nil { t.Errorf("testcase %d: got error: %v", idx, err) continue } fn := topFrameFunction(data.Stack) if !strings.Contains(fn, tc.ExpectTopFrame) { t.Errorf("testcase %d: expected %s got %s", idx, tc.ExpectTopFrame, fn) } } } go-agent-3.42.0/v3/newrelic/internal_errors_test.go000066400000000000000000000545341510742411500222520ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "runtime" "strconv" "testing" "github.com/newrelic/go-agent/v3/internal" ) type myError struct{} func (e myError) Error() string { return "my msg" } func TestNoticeErrorBackground(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorWeb(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: helloRequestAttributes, }}) app.ExpectMetrics(t, webErrorMetrics) } func TestNoticeErrorTxnEnded(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.End() txn.NoticeError(myError{}) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestNoticeErrorHighSecurity(t *testing.T) { cfgFn := func(cfg *Config) { cfg.HighSecurity = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: highSecurityErrorMsg, Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": highSecurityErrorMsg, "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorMessageSecurityPolicy(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.AllowRawExceptionMessages.SetEnabled(false) } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: securityPolicyErrorMsg, Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": securityPolicyErrorMsg, "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorLocallyDisabled(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ErrorCollector.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": errorsDisabled.Error(), }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestErrorsDisabledByServerSideConfig(t *testing.T) { // Test that errors can be disabled by server-side-config. cfgFn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"error_collector.enabled":false}}`), reply) } app := testApp(replyfn, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": errorsDisabled.Error(), }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestErrorsEnabledByServerSideConfig(t *testing.T) { // Test that errors can be enabled by server-side-config. cfgFn := func(cfg *Config) { cfg.ErrorCollector.Enabled = false cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"error_collector.enabled":true}}`), reply) } app := testApp(replyfn, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorTracedErrorsRemotelyDisabled(t *testing.T) { // This tests that the connect reply field "collect_errors" controls the // collection of traced-errors, not error-events. replyfn := func(reply *internal.ConnectReply) { reply.CollectErrors = false } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorNil(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(nil) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": errNilError.Error(), }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestNoticeErrorEventsLocallyDisabled(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ErrorCollector.CaptureEvents = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNoticeErrorEventsRemotelyDisabled(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.CollectErrorEvents = false } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundErrorMetrics) } type errorWithClass struct{ class string } func (e errorWithClass) Error() string { return "my msg" } func (e errorWithClass) ErrorClass() string { return e.class } func TestErrorWithClasser(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(errorWithClass{class: "zap"}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "zap", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "zap", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestErrorWithClasserReturnsEmpty(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(errorWithClass{class: ""}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.errorWithClass", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.errorWithClass", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } type withStackTrace struct{ trace []uintptr } func makeErrorWithStackTrace() error { callers := make([]uintptr, 20) written := runtime.Callers(1, callers) return withStackTrace{ trace: callers[0:written], } } func (e withStackTrace) Error() string { return "my msg" } func (e withStackTrace) StackTrace() []uintptr { return e.trace } func TestErrorWithStackTrace(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") e := makeErrorWithStackTrace() txn.NoticeError(e) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.withStackTrace", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.withStackTrace", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestErrorWithStackTraceReturnsNil(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") e := withStackTrace{trace: nil} txn.NoticeError(e) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.withStackTrace", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.withStackTrace", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorNoAttributes(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "my class", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorValidAttributes(t *testing.T) { extraAttributes := map[string]interface{}{ "zip": "zap", } app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: extraAttributes, }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "my class", UserAttributes: extraAttributes, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: extraAttributes, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorAttributesHighSecurity(t *testing.T) { extraAttributes := map[string]interface{}{ "zip": "zap", } cfgFn := func(cfg *Config) { cfg.HighSecurity = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: extraAttributes, }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "message removed by high security setting", Klass: "my class", UserAttributes: map[string]interface{}{}, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "message removed by high security setting", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{}, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorAttributesSecurityPolicy(t *testing.T) { extraAttributes := map[string]interface{}{ "zip": "zap", } replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.CustomParameters.SetEnabled(false) } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: extraAttributes, }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "my class", UserAttributes: map[string]interface{}{}, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{}, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorAttributeOverridesNormalAttribute(t *testing.T) { extraAttributes := map[string]interface{}{ "zip": "zap", } app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.AddAttribute("zip", 123) txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: extraAttributes, }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "my class", UserAttributes: extraAttributes, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: extraAttributes, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestNewrelicErrorInvalidAttributes(t *testing.T) { extraAttributes := map[string]interface{}{ "zip": "zap", "INVALID": struct{}{}, } app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: extraAttributes, }) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": `attribute 'INVALID' value of type struct {} is invalid`, }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestExtraErrorAttributeRemovedThroughConfiguration(t *testing.T) { cfgfn := func(cfg *Config) { cfg.ErrorCollector.Attributes.Exclude = []string{"IGNORE_ME"} cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: map[string]interface{}{ "zip": "zap", "IGNORE_ME": 123, }, }) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "my class", UserAttributes: map[string]interface{}{"zip": "zap"}, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "my class", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{"zip": "zap"}, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestTooManyExtraErrorAttributes(t *testing.T) { attrs := make(map[string]interface{}) for i := 0; i <= attributeErrorLimit; i++ { attrs[strconv.Itoa(i)] = i } app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(Error{ Message: "my msg", Class: "my class", Attributes: attrs, }) app.expectSingleLoggedError(t, "unable to notice error", map[string]interface{}{ "reason": errTooManyErrorAttributes.Error(), }) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } type basicError struct{} func (e basicError) Error() string { return "something went wrong" } type withClass struct{ class string } func (e withClass) Error() string { return "something went wrong" } func (e withClass) ErrorClass() string { return e.class } type withClassAndCause struct { cause error class string } func (e withClassAndCause) Error() string { return e.cause.Error() } func (e withClassAndCause) Unwrap() error { return e.cause } func (e withClassAndCause) ErrorClass() string { return e.class } type withCause struct{ cause error } func (e withCause) Error() string { return e.cause.Error() } func (e withCause) Unwrap() error { return e.cause } func errWithClass(class string) error { return withClass{class: class} } func wrapWithClass(e error, class string) error { return withClassAndCause{cause: e, class: class} } func wrapError(e error) error { return withCause{cause: e} } func TestErrorClass(t *testing.T) { // First choice is any ErrorClass() of the immediate error. // Second choice is any ErrorClass() of the error's cause. // Final choice is the reflect type of the error's cause. testcases := []struct { Error error Expect string }{ {Error: basicError{}, Expect: "newrelic.basicError"}, {Error: errWithClass("zap"), Expect: "zap"}, {Error: errWithClass(""), Expect: "newrelic.withClass"}, {Error: wrapWithClass(errWithClass("zap"), "zip"), Expect: "zip"}, {Error: wrapWithClass(errWithClass("zap"), ""), Expect: "zap"}, {Error: wrapWithClass(errWithClass(""), ""), Expect: "newrelic.withClass"}, {Error: wrapError(basicError{}), Expect: "newrelic.basicError"}, {Error: wrapError(errWithClass("zap")), Expect: "zap"}, } for idx, tc := range testcases { data, err := errDataFromError(tc.Error, false) if err != nil { t.Errorf("testcase %d: got error: %v", idx, err) continue } if data.Klass != tc.Expect { t.Errorf("testcase %d: expected %s got %s", idx, tc.Expect, data.Klass) } } } func TestNoticeErrorSpanID(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "guid": "52fdfc072182654f", "priority": 1.437714, "sampled": true, "spanId": "9566c74d10d1e2c6", "traceId": "52fdfc072182654f163f5f0f9a621d72", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetricsUnknownCaller) } func TestNoticeErrorWriteHeaderSpanID(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.SetWebResponse(nil).WriteHeader(500) app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "Internal Server Error", Klass: "500", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "500", "error.message": "Internal Server Error", "guid": "52fdfc072182654f", "priority": 1.437714, "sampled": true, "spanId": "9566c74d10d1e2c6", "traceId": "52fdfc072182654f163f5f0f9a621d72", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetricsUnknownCaller) } func TestNoticeErrorPanicRecoverySpanID(t *testing.T) { cfgfn := func(cfg *Config) { enableBetterCAT(cfg) cfg.ErrorCollector.RecordPanics = true } app := testApp(distributedTracingReplyFields, cfgfn, t) func() { defer func() { if recovered := recover(); recovered == nil { t.Error("no panic recovered") } }() txn := app.StartTransaction("hello") defer txn.End() panic("oops") }() app.expectNoLoggedErrors(t) app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "oops", Klass: "panic", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "panic", "error.message": "oops", "guid": "52fdfc072182654f", "priority": 1.437714, "sampled": true, "spanId": "9566c74d10d1e2c6", "traceId": "52fdfc072182654f163f5f0f9a621d72", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetricsUnknownCaller) } go-agent-3.42.0/v3/newrelic/internal_response_writer.go000066400000000000000000000076301510742411500231240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bufio" "io" "net" "net/http" ) type replacementResponseWriter struct { thd *thread original http.ResponseWriter } func (rw *replacementResponseWriter) Header() http.Header { return rw.original.Header() } func (rw *replacementResponseWriter) Write(b []byte) (n int, err error) { hdr := rw.original.Header() // This is safe to call unconditionally, even if Write() is called multiple // times; see also the commentary in addCrossProcessHeaders(). addCrossProcessHeaders(rw.thd.txn, hdr) n, err = rw.original.Write(b) headersJustWritten(rw.thd, http.StatusOK, hdr) if IsSecurityAgentPresent() { secureAgent.SendEvent("INBOUND_WRITE", string(b), hdr, rw.thd.GetLinkingMetadata().TraceID) } return } func (rw *replacementResponseWriter) WriteHeader(code int) { hdr := rw.original.Header() addCrossProcessHeaders(rw.thd.txn, hdr) rw.original.WriteHeader(code) headersJustWritten(rw.thd, code, hdr) if IsSecurityAgentPresent() { secureAgent.SendEvent("INBOUND_RESPONSE_CODE", code) } } func (rw *replacementResponseWriter) CloseNotify() <-chan bool { return rw.original.(http.CloseNotifier).CloseNotify() } func (rw *replacementResponseWriter) Flush() { rw.original.(http.Flusher).Flush() } func (rw *replacementResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return rw.original.(http.Hijacker).Hijack() } func (rw *replacementResponseWriter) ReadFrom(r io.Reader) (int64, error) { return rw.original.(io.ReaderFrom).ReadFrom(r) } func upgradeResponseWriter(rw *replacementResponseWriter) http.ResponseWriter { // GENERATED CODE DO NOT MODIFY // This code generated by internal/tools/interface-wrapping var ( i0 int32 = 1 << 0 i1 int32 = 1 << 1 i2 int32 = 1 << 2 i3 int32 = 1 << 3 ) var interfaceSet int32 if _, ok := rw.original.(http.CloseNotifier); ok { interfaceSet |= i0 } if _, ok := rw.original.(http.Flusher); ok { interfaceSet |= i1 } if _, ok := rw.original.(http.Hijacker); ok { interfaceSet |= i2 } if _, ok := rw.original.(io.ReaderFrom); ok { interfaceSet |= i3 } switch interfaceSet { default: // No optional interfaces implemented return struct { http.ResponseWriter }{rw} case i0: return struct { http.ResponseWriter http.CloseNotifier }{rw, rw} case i1: return struct { http.ResponseWriter http.Flusher }{rw, rw} case i0 | i1: return struct { http.ResponseWriter http.CloseNotifier http.Flusher }{rw, rw, rw} case i2: return struct { http.ResponseWriter http.Hijacker }{rw, rw} case i0 | i2: return struct { http.ResponseWriter http.CloseNotifier http.Hijacker }{rw, rw, rw} case i1 | i2: return struct { http.ResponseWriter http.Flusher http.Hijacker }{rw, rw, rw} case i0 | i1 | i2: return struct { http.ResponseWriter http.CloseNotifier http.Flusher http.Hijacker }{rw, rw, rw, rw} case i3: return struct { http.ResponseWriter io.ReaderFrom }{rw, rw} case i0 | i3: return struct { http.ResponseWriter http.CloseNotifier io.ReaderFrom }{rw, rw, rw} case i1 | i3: return struct { http.ResponseWriter http.Flusher io.ReaderFrom }{rw, rw, rw} case i0 | i1 | i3: return struct { http.ResponseWriter http.CloseNotifier http.Flusher io.ReaderFrom }{rw, rw, rw, rw} case i2 | i3: return struct { http.ResponseWriter http.Hijacker io.ReaderFrom }{rw, rw, rw} case i0 | i2 | i3: return struct { http.ResponseWriter http.CloseNotifier http.Hijacker io.ReaderFrom }{rw, rw, rw, rw} case i1 | i2 | i3: return struct { http.ResponseWriter http.Flusher http.Hijacker io.ReaderFrom }{rw, rw, rw, rw} case i0 | i1 | i2 | i3: return struct { http.ResponseWriter http.CloseNotifier http.Flusher http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw} } } go-agent-3.42.0/v3/newrelic/internal_response_writer_test.go000066400000000000000000000051331510742411500241570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bufio" "io" "net" "net/http" "testing" ) type rwNoExtraMethods struct { hijackCalled bool readFromCalled bool flushCalled bool closeNotifyCalled bool } type rwTwoExtraMethods struct{ rwNoExtraMethods } type rwAllExtraMethods struct{ rwTwoExtraMethods } func (rw *rwAllExtraMethods) CloseNotify() <-chan bool { rw.closeNotifyCalled = true return nil } func (rw *rwAllExtraMethods) ReadFrom(r io.Reader) (int64, error) { rw.readFromCalled = true return 0, nil } func (rw *rwNoExtraMethods) Header() http.Header { return nil } func (rw *rwNoExtraMethods) Write([]byte) (int, error) { return 0, nil } func (rw *rwNoExtraMethods) WriteHeader(statusCode int) {} func (rw *rwTwoExtraMethods) Flush() { rw.flushCalled = true } func (rw *rwTwoExtraMethods) Hijack() (net.Conn, *bufio.ReadWriter, error) { rw.hijackCalled = true return nil, nil, nil } func TestTransactionAllExtraMethods(t *testing.T) { app := testApp(nil, nil, t) rw := &rwAllExtraMethods{} txn := app.StartTransaction("hello") w := txn.SetWebResponse(rw) if v, ok := w.(http.CloseNotifier); ok { v.CloseNotify() } if v, ok := w.(http.Flusher); ok { v.Flush() } if v, ok := w.(http.Hijacker); ok { v.Hijack() } if v, ok := w.(io.ReaderFrom); ok { v.ReadFrom(nil) } if !rw.hijackCalled || !rw.readFromCalled || !rw.flushCalled || !rw.closeNotifyCalled { t.Error("wrong methods called", rw) } } func TestTransactionNoExtraMethods(t *testing.T) { app := testApp(nil, nil, t) rw := &rwNoExtraMethods{} txn := app.StartTransaction("hello") w := txn.SetWebResponse(rw) if _, ok := w.(http.CloseNotifier); ok { t.Error("unexpected CloseNotifier method") } if _, ok := w.(http.Flusher); ok { t.Error("unexpected Flusher method") } if _, ok := w.(http.Hijacker); ok { t.Error("unexpected Hijacker method") } if _, ok := w.(io.ReaderFrom); ok { t.Error("unexpected ReaderFrom method") } } func TestTransactionTwoExtraMethods(t *testing.T) { app := testApp(nil, nil, t) rw := &rwTwoExtraMethods{} txn := app.StartTransaction("hello") w := txn.SetWebResponse(rw) if _, ok := w.(http.CloseNotifier); ok { t.Error("unexpected CloseNotifier method") } if v, ok := w.(http.Flusher); ok { v.Flush() } if v, ok := w.(http.Hijacker); ok { v.Hijack() } if _, ok := w.(io.ReaderFrom); ok { t.Error("unexpected ReaderFrom method") } if !rw.hijackCalled || rw.readFromCalled || !rw.flushCalled || rw.closeNotifyCalled { t.Error("wrong methods called", rw) } } go-agent-3.42.0/v3/newrelic/internal_segment_attributes_test.go000066400000000000000000000426321510742411500246420ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "net/http" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) func TestTraceSegments(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 0 cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 // Disable span event attributes to ensure they are separate. cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "aws.region": "west", }, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "query_parameters": "map[zap:zip]", "peer.address": "myhost:myport", "peer.hostname": "myhost", "db.statement": "myquery", "db.instance": "dbname", "aws.requestId": 123, }, }, { SegmentName: "External/example.com/http/GET", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "http.url": "http://example.com", "aws.operation": "secret", }, }, }, }}, }, }}) } func TestTraceSegmentsNoBacktrace(t *testing.T) { // Test that backtrace will only appear if the segment's duration // exceeds TransactionTracer.Segments.StackTraceThreshold. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 1 * time.Hour cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 // Disable span event attributes to ensure they are separate. cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{ "aws.region": "west", }, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{ "query_parameters": "map[zap:zip]", "peer.address": "myhost:myport", "peer.hostname": "myhost", "db.statement": "myquery", "db.instance": "dbname", "aws.requestId": 123, }, }, { SegmentName: "External/example.com/http/GET", Attributes: map[string]interface{}{ "http.url": "http://example.com", "aws.operation": "secret", }, }, }, }}, }, }}) } func TestTraceStacktraceServerSideConfig(t *testing.T) { // Test that the server-side-config stack trace threshold is observed. replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.stack_trace_threshold":0}}`), reply) } cfgfn := func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 1 * time.Hour cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.DistributedTracer.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") basicSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, }, }, }, }}, }, }}) } func TestTraceSegmentAttributesExcluded(t *testing.T) { // Test that segment attributes can be excluded by Attributes.Exclude. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 1 * time.Hour cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.Attributes.Exclude = []string{ SpanAttributeDBStatement, SpanAttributeDBInstance, SpanAttributeDBCollection, SpanAttributePeerAddress, SpanAttributePeerHostname, SpanAttributeHTTPURL, SpanAttributeHTTPMethod, SpanAttributeAWSOperation, SpanAttributeAWSRequestID, SpanAttributeAWSRegion, "query_parameters", } // Disable span event attributes to ensure they are separate. cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{}, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{}, }, { SegmentName: "External/example.com/http/GET", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestTraceSegmentAttributesSpecificallyExcluded(t *testing.T) { // Test that segment attributes can be excluded by // TransactionTracer.Segments.Attributes.Exclude. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 1 * time.Hour cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Attributes.Exclude = []string{ SpanAttributeDBStatement, SpanAttributeDBInstance, SpanAttributeDBCollection, SpanAttributePeerAddress, SpanAttributePeerHostname, SpanAttributeHTTPURL, SpanAttributeHTTPMethod, SpanAttributeAWSOperation, SpanAttributeAWSRequestID, SpanAttributeAWSRegion, "query_parameters", } // Disable span event attributes to ensure they are separate. cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{}, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{}, }, { SegmentName: "External/example.com/http/GET", Attributes: map[string]interface{}{}, }, }, }}, }, }}) } func TestTraceSegmentAttributesDisabled(t *testing.T) { // Test that segment attributes can be disabled by Attributes.Enabled // but backtrace and transaction_guid still appear. cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = true cfg.Attributes.Enabled = false cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 0 cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 } app := testApp(crossProcessReplyFn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) externalSegment.Response = &http.Response{ Header: outboundCrossProcessResponse(), } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, }, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, }, }, { SegmentName: "ExternalTransaction/example.com/12345#67890/WebTransaction/Go/txn", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "transaction_guid": internal.MatchAnything, }, }, }, }}, }, }}) } func TestTraceSegmentAttributesSpecificallyDisabled(t *testing.T) { // Test that segment attributes can be disabled by // TransactionTracer.Segments.Attributes.Enabled but backtrace and // transaction_guid still appear. cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = true cfg.TransactionTracer.Segments.Attributes.Enabled = false cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Segments.StackTraceThreshold = 0 cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 } app := testApp(crossProcessReplyFn, cfgfn, t) txn := app.StartTransaction("hello") basicSegment := txn.StartSegment("basic") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") basicSegment.End() datastoreSegment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", QueryParameters: map[string]interface{}{"zap": "zip"}, } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") datastoreSegment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) externalSegment := StartExternalSegment(txn, req) externalSegment.Response = &http.Response{ Header: outboundCrossProcessResponse(), } internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") externalSegment.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "OtherTransaction/Go/hello", Root: internal.WantTraceSegment{ SegmentName: "ROOT", Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "OtherTransaction/Go/hello", Attributes: map[string]interface{}{"exclusive_duration_millis": internal.MatchAnything}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/basic", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, }, }, { SegmentName: "Datastore/statement/MySQL/mycollection/myoperation", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, }, }, { SegmentName: "ExternalTransaction/example.com/12345#67890/WebTransaction/Go/txn", Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "transaction_guid": internal.MatchAnything, }, }, }, }}, }, }}) } go-agent-3.42.0/v3/newrelic/internal_serverless_test.go000066400000000000000000000262431510742411500231270ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "net/http" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) func TestServerlessDistributedTracingConfigPresent(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = true cfg.ServerlessMode.AccountID = "123" cfg.ServerlessMode.TrustedAccountKey = "987" cfg.ServerlessMode.PrimaryAppID = "456" } app := testApp(nil, cfgFn, t) hdrs := http.Header{} app.StartTransaction("hello").InsertDistributedTraceHeaders(hdrs) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: singleCount}, }) } func TestServerlessDistributedTracingConfigPartiallyPresent(t *testing.T) { // This tests that if ServerlessMode.PrimaryAppID is unset it should // default to "Unknown". cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = true cfg.ServerlessMode.AccountID = "123" cfg.ServerlessMode.TrustedAccountKey = "987" } app := testApp(nil, cfgFn, t) hdrs := http.Header{} app.StartTransaction("hello").InsertDistributedTraceHeaders(hdrs) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: singleCount}, }) } func TestServerlessDistributedTracingConfigTrustKeyAbsent(t *testing.T) { // Test that distributed tracing works if only AccountID has been set. cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = true cfg.ServerlessMode.AccountID = "123" } app := testApp(nil, cfgFn, t) hdrs := http.Header{} app.StartTransaction("hello").InsertDistributedTraceHeaders(hdrs) txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/App/123/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/Unknown/HTTP/allOther", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: singleCount}, }) } func TestServerlessDistributedTracingConfigAbsent(t *testing.T) { // Test that payloads do not get created or accepted when distributed // tracing configuration is not present. cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = true } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") emptyHdrs := http.Header{} txn.InsertDistributedTraceHeaders(emptyHdrs) if len(emptyHdrs) != 0 { t.Error(emptyHdrs) } nonEmptyHdrs := http.Header{} app2 := testApp(nil, func(cfg *Config) { cfgFn(cfg) cfg.ServerlessMode.AccountID = "123" cfg.ServerlessMode.TrustedAccountKey = "trustkey" cfg.ServerlessMode.PrimaryAppID = "456" }, t) app2.StartTransaction("hello").InsertDistributedTraceHeaders(nonEmptyHdrs) if len(nonEmptyHdrs) == 0 { t.Error(nonEmptyHdrs) } txn.AcceptDistributedTraceHeaders(TransportHTTP, nonEmptyHdrs) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) } func TestServerlessLowApdex(t *testing.T) { apdex := -1 * time.Second cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.ServerlessMode.ApdexThreshold = apdex cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(nil) // only web gets apdex txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, // third apdex field is failed count {Name: "Apdex", Scope: "", Forced: true, Data: []float64{0, 0, 1, apdex.Seconds(), apdex.Seconds(), 0}}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: []float64{0, 0, 1, apdex.Seconds(), apdex.Seconds(), 0}}, }) } func TestServerlessHighApdex(t *testing.T) { apdex := 1 * time.Hour cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.ServerlessMode.ApdexThreshold = apdex cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(nil) // only web gets apdex txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, // first apdex field is satisfied count {Name: "Apdex", Scope: "", Forced: true, Data: []float64{1, 0, 0, apdex.Seconds(), apdex.Seconds(), 0}}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: []float64{1, 0, 0, apdex.Seconds(), apdex.Seconds(), 0}}, }) } func TestServerlessRecordCustomMetric(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) app.RecordCustomMetric("myMetric", 123.0) app.expectSingleLoggedError(t, "unable to record custom metric", map[string]interface{}{ "metric-name": "myMetric", "reason": errMetricServerless.Error(), }) } func TestServerlessRecordCustomEvent(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) attributes := map[string]interface{}{"zip": 1} app.RecordCustomEvent("myType", attributes) app.expectNoLoggedErrors(t) app.ExpectCustomEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "myType", "timestamp": internal.MatchAnything, }, UserAttributes: attributes, }}) buf := &bytes.Buffer{} internal.ServerlessWrite(app.Application.Private, "my-arn", buf) _, data, err := parseServerlessPayload(buf.Bytes()) if err != nil { t.Fatal(err) } // Data should contain only custom events. Dynamic timestamp makes exact // comparison difficult. eventData := string(data["custom_event_data"]) if !strings.Contains(eventData, `{"zip":1}`) { t.Error(eventData) } if len(data) != 1 { t.Fatal(data) } } func TestServerlessJSON(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.Private.(internal.AddAgentAttributer).AddAgentAttribute(AttributeAWSLambdaARN, "thearn", nil) txn.End() buf := &bytes.Buffer{} internal.ServerlessWrite(app.Application.Private, "lambda-test-arn", buf) metadata, data, err := parseServerlessPayload(buf.Bytes()) if err != nil { t.Fatal(err) } // Data should contain txn event and metrics. Timestamps make exact // JSON comparison tough. if v := data["metric_data"]; nil == v { t.Fatal(data) } if v := data["analytic_event_data"]; nil == v { t.Fatal(data) } if v := string(metadata["arn"]); v != `"lambda-test-arn"` { t.Fatal(v) } if v := string(metadata["agent_version"]); v != `"`+Version+`"` { t.Fatal(v) } } func TestServerlessConnectReply(t *testing.T) { cfg := config{Config: defaultConfig()} cfg.ServerlessMode.ApdexThreshold = 2 * time.Second cfg.ServerlessMode.AccountID = "the-account-id" cfg.ServerlessMode.TrustedAccountKey = "the-trust-key" cfg.ServerlessMode.PrimaryAppID = "the-primary-app" reply := newServerlessConnectReply(cfg) if reply.ApdexThresholdSeconds != 2 { t.Error(reply.ApdexThresholdSeconds) } if reply.AccountID != "the-account-id" { t.Error(reply.AccountID) } if reply.TrustedAccountKey != "the-trust-key" { t.Error(reply.TrustedAccountKey) } if reply.PrimaryAppID != "the-primary-app" { t.Error(reply.PrimaryAppID) } if reply.SamplingTargetPeriodInSeconds != 60 { t.Error(reply.SamplingTargetPeriodInSeconds) } if reply.SamplingTarget != 10 { t.Error(reply.SamplingTarget) } // Now test the defaults: cfg = config{Config: defaultConfig()} reply = newServerlessConnectReply(cfg) if reply.ApdexThresholdSeconds != 0.5 { t.Error(reply.ApdexThresholdSeconds) } if reply.AccountID != "" { t.Error(reply.AccountID) } if reply.TrustedAccountKey != "" { t.Error(reply.TrustedAccountKey) } if reply.PrimaryAppID != "Unknown" { t.Error(reply.PrimaryAppID) } } go-agent-3.42.0/v3/newrelic/internal_set_web_request_test.go000066400000000000000000000277441510742411500241410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "net/url" "testing" "github.com/newrelic/go-agent/v3/internal" ) var ( sampleHTTPRequest = func() *http.Request { req, err := http.NewRequest("GET", "http://www.newrelic.com", nil) if nil != err { panic(err) } req.Header.Set("Accept", "myaccept") req.Header.Set("Content-Type", "mycontent") req.Header.Set("Content-Length", "123") //we should pull the host from the request field, not the headers req.Header.Set("Host", "wrongHost") req.Host = "myhost" return req }() sampleCustomRequest = func() WebRequest { u, err := url.Parse("http://www.newrelic.com") if nil != err { panic(err) } hdr := make(http.Header) hdr.Set("Accept", "myaccept") hdr.Set("Content-Type", "mycontent") hdr.Set("Content-Length", "123") //we should pull the host from the request field, not the headers hdr.Set("Host", "wrongHost") return WebRequest{ Header: hdr, URL: u, Method: "GET", Transport: TransportHTTP, Host: "myhost", } }() sampleRequestAgentAttributes = map[string]interface{}{ AttributeRequestMethod: "GET", AttributeRequestAccept: "myaccept", AttributeRequestContentType: "mycontent", AttributeRequestContentLength: 123, AttributeRequestHost: "myhost", AttributeRequestURI: "http://www.newrelic.com", } ) func TestSetWebRequestNil(t *testing.T) { // Test that using SetWebRequest with nil marks the transaction as a web // transaction. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(nil) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, }}) } func TestSetWebRequestHTTPNil(t *testing.T) { // Test that calling NewWebRequestHTTP with a nil pointer and sets // the transaction as a web transaction. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(nil) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, }}) } func TestSetWebRequestHTTPRequest(t *testing.T) { // Test that SetWebRequestHTTP uses the *http.Request as expected. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(sampleHTTPRequest) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: sampleRequestAgentAttributes, Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, }}) } func TestSetWebRequestAlreadyEnded(t *testing.T) { // Test that SetWebRequest returns an error if called after // Transaction.End. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.End() txn.SetWebRequest(sampleCustomRequest) app.expectSingleLoggedError(t, "unable to set web request", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, }, }}) } func TestSetWebRequestWithDistributedTracing(t *testing.T) { // Test that the WebRequest.Transport value is used as the // distributed tracing transport if a distributed tracing header is // found in the WebRequest.Header. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) hdrs := http.Header{} app.StartTransaction("hello").InsertDistributedTraceHeaders(hdrs) // Copy sampleCustomRequest to avoid modifying it since it is used in // other tests. req := sampleCustomRequest req.Header = hdrs txn := app.StartTransaction("hello") txn.SetWebRequest(req) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/all", Scope: "", Forced: false, Data: nil}, {Name: "TransportDuration/App/123/456/HTTP/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "Supportability/TraceContext/Accept/Success", Scope: "", Forced: true, Data: singleCount}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://www.newrelic.com", "request.headers.host": "myhost", }, Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "parent.type": "App", "parent.account": "123", "parent.app": "456", "parent.transportType": "HTTP", "parent.transportDuration": internal.MatchAnything, "parentId": internal.MatchAnything, "traceId": internal.MatchAnything, "parentSpanId": internal.MatchAnything, "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, }}) app.ExpectSpanEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "category": "generic", "guid": internal.MatchAnything, "name": "WebTransaction/Go/hello", "nr.entryPoint": true, "parentId": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "transaction.name": "WebTransaction/Go/hello", "trustedParentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "parent.account": "123", "parent.app": "456", "parent.transportDuration": internal.MatchAnything, "parent.transportType": "HTTP", "parent.type": "App", "request.method": "GET", "request.uri": "http://www.newrelic.com", "request.headers.host": "myhost", }, }}) } func TestSetWebRequestIncompleteRequest(t *testing.T) { // Test SetWebRequest will safely handle situations where the request's // URL and Header values are nil. app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") txn.SetWebRequest(WebRequest{Transport: TransportUnknown}) app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, }) app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "guid": internal.MatchAnything, "sampled": internal.MatchAnything, "priority": internal.MatchAnything, "traceId": internal.MatchAnything, "nr.apdexPerfZone": internal.MatchAnything, }, }}) } go-agent-3.42.0/v3/newrelic/internal_set_web_response_test.go000066400000000000000000000046341510742411500243000ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "net/http/httptest" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestSetWebResponseNil(t *testing.T) { // Test that the methods of the txn.SetWebResponse(nil) return value // writer can safely be called. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") rw := txn.SetWebResponse(nil) rw.WriteHeader(123) if hdr := rw.Header(); hdr != nil { t.Error(hdr) } n, err := rw.Write([]byte("should not panic")) if err != nil || n != 0 { t.Error(err, n) } txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{ "httpResponseCode": 123, "http.statusCode": 123, }, Intrinsics: map[string]interface{}{"name": "OtherTransaction/Go/hello"}, }}) } func TestSetWebResponseSuccess(t *testing.T) { // Test that the return value of txn.SetWebResponse delegates to the // input writer. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") w := httptest.NewRecorder() rw := txn.SetWebResponse(w) rw.WriteHeader(123) hdr := rw.Header() hdr.Set("zip", "zap") body := "should not panic" n, err := rw.Write([]byte(body)) if err != nil || n != len(body) { t.Error(err, n) } txn.End() if w.Code != 123 { t.Error(w.Code) } if w.HeaderMap.Get("zip") != "zap" { t.Error(w.HeaderMap) } if w.Body.String() != body { t.Error(w.Body.String()) } app.ExpectTxnEvents(t, []internal.WantEvent{{ AgentAttributes: map[string]interface{}{ "httpResponseCode": 123, "http.statusCode": 123, }, Intrinsics: map[string]interface{}{"name": "OtherTransaction/Go/hello"}, }}) } type writerWithFlush struct{} func (w writerWithFlush) Header() http.Header { return nil } func (w writerWithFlush) WriteHeader(int) {} func (w writerWithFlush) Write([]byte) (int, error) { return 0, nil } func (w writerWithFlush) Flush() {} func TestSetWebResponseTxnUpgraded(t *testing.T) { // Test that the writer returned by SetWebResponse has the optional // methods of the input writer. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") rw := txn.SetWebResponse(writerWithFlush{}) if _, ok := rw.(http.Flusher); !ok { t.Error("should have Flusher now") } } go-agent-3.42.0/v3/newrelic/internal_slow_queries_test.go000066400000000000000000000676011510742411500234560ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestSlowQueryBasic(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) } func TestSlowQueryLocallyDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.SlowQuery.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{}) } func TestSlowQueryRemotelyDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { reply.CollectTraces = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{}) } func TestSlowQueryBelowThreshold(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 1 * time.Hour cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{}) } func TestSlowQueryDatabaseProvided(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.DatastoreTracer.SlowQuery.Threshold = 0 } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", DatabaseName: "my_database", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "my_database", Host: "", PortPathOrID: "", }}) } func TestSlowQueryHostProvided(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "db-server-1", PortPathOrID: "unknown", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: scope, Forced: false, Data: nil}, {Name: "Datastore/instance/MySQL/db-server-1/unknown", Scope: "", Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryPortProvided(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", PortPathOrID: "98021", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "unknown", PortPathOrID: "98021", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: scope, Forced: false, Data: nil}, {Name: "Datastore/instance/MySQL/unknown/98021", Scope: "", Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryHostPortProvided(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", PortPathOrID: "98021", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "db-server-1", PortPathOrID: "98021", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: scope, Forced: false, Data: nil}, {Name: "Datastore/instance/MySQL/db-server-1/98021", Scope: "", Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryAggregation(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) ds := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } ds.End() ds = DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", } ds.End() ds = DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastorePostgres, Collection: "products", Operation: "INSERT", ParameterizedQuery: "INSERT INTO products (name, price) VALUES ($1, $2)", } ds.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 2, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }, { Count: 1, MetricName: "Datastore/statement/Postgres/products/INSERT", Query: "INSERT INTO products (name, price) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }, }) } func TestSlowQueryMissingQuery(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "'INSERT' on 'users' using 'MySQL'", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) } func TestSlowQueryMissingEverything(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/operation/Unknown/other", Query: "'other' on 'unknown' using 'Unknown'", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/Unknown/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/Unknown/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/Unknown/other", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/Unknown/other", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryWithQueryParameters(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: params, }}) } func TestSlowQueryHighSecurity(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.HighSecurity = true cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: nil, }}) } func TestSlowQuerySecurityPolicyFalse(t *testing.T) { // When the record_sql security policy is set to false, sql parameters // and the sql format string should be replaced. cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.RecordSQL.SetEnabled(false) } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "'INSERT' on 'users' using 'MySQL'", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: nil, }}) } func TestSlowQuerySecurityPolicyTrue(t *testing.T) { // When the record_sql security policy is set to true, sql parameters // should be omitted. cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.RecordSQL.SetEnabled(true) } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: nil, }}) } func TestSlowQueryInvalidParameters(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, "invalid_value": struct{}{}, strings.Repeat("key-too-long", 100): 1, "long-key": strings.Repeat("A", 300), } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: map[string]interface{}{ "str": "zap", "int": 123, "long-key": strings.Repeat("A", 255), }, }}) } func TestSlowQueryParametersDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.QueryParameters.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) params := map[string]interface{}{ "str": "zap", "int": 123, } s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: params, } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: nil, }}) } func TestSlowQueryInstanceDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.InstanceReporting.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryInstanceDisabledLocalhost(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.InstanceReporting.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "localhost", PortPathOrID: "3306", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/users/INSERT", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestSlowQueryDatabaseNameDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.DatabaseNameReporting.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", DatabaseName: "db-server-1", } s1.End() txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", }}) } func TestDatastoreAPICrossAgent(t *testing.T) { var testcases []struct { TestName string `json:"test_name"` Input struct { Parameters struct { Product string `json:"product"` Collection string `json:"collection"` Operation string `json:"operation"` Host string `json:"host"` PortPathOrID string `json:"port_path_or_id"` DatabaseName string `json:"database_name"` } `json:"parameters"` IsWeb bool `json:"is_web"` SystemHostname string `json:"system_hostname"` Configuration struct { InstanceEnabled bool `json:"datastore_tracer.instance_reporting.enabled"` DatabaseEnabled bool `json:"datastore_tracer.database_name_reporting.enabled"` } } Expectation struct { MetricsScoped []string `json:"metrics_scoped"` MetricsUnscoped []string `json:"metrics_unscoped"` Trace struct { MetricName string `json:"metric_name"` Host string `json:"host"` PortPathOrID string `json:"port_path_or_id"` DatabaseName string `json:"database_name"` } `json:"transaction_segment_and_slow_query_trace"` } } err := crossagent.ReadJSON("datastores/datastore_api.json", &testcases) if err != nil { t.Fatal(err) } for _, tc := range testcases { query := "my query" cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DatastoreTracer.InstanceReporting.Enabled = tc.Input.Configuration.InstanceEnabled cfg.DatastoreTracer.DatabaseNameReporting.Enabled = tc.Input.Configuration.DatabaseEnabled cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") var txnURL string if tc.Input.IsWeb { txnURL = helloPath txn.SetWebRequestHTTP(helloRequest) } ds := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreProduct(tc.Input.Parameters.Product), Operation: tc.Input.Parameters.Operation, Collection: tc.Input.Parameters.Collection, PortPathOrID: tc.Input.Parameters.PortPathOrID, Host: tc.Input.Parameters.Host, DatabaseName: tc.Input.Parameters.DatabaseName, ParameterizedQuery: query, } ds.End() txn.End() var metrics []internal.WantMetric var scope string if tc.Input.IsWeb { scope = "WebTransaction/Go/hello" metrics = append([]internal.WantMetric{}, webMetrics...) } else { scope = "OtherTransaction/Go/hello" metrics = append([]internal.WantMetric{}, backgroundMetrics...) } for _, m := range tc.Expectation.MetricsScoped { metrics = append(metrics, internal.WantMetric{ Name: m, Scope: scope, Forced: nil, Data: nil, }) } for _, m := range tc.Expectation.MetricsUnscoped { metrics = append(metrics, internal.WantMetric{ Name: m, Scope: "", Forced: nil, Data: nil, }) } expectTraceHost := tc.Expectation.Trace.Host host := txn.thread.Config.hostname if tc.Input.SystemHostname != "" { for i := range metrics { metrics[i].Name = strings.Replace(metrics[i].Name, tc.Input.SystemHostname, host, -1) } expectTraceHost = strings.Replace(expectTraceHost, tc.Input.SystemHostname, host, -1) } tt := extendValidator(t, tc.TestName) app.ExpectMetrics(tt, metrics) app.ExpectSlowQueries(tt, []internal.WantSlowQuery{{ Count: 1, MetricName: tc.Expectation.Trace.MetricName, TxnName: scope, DatabaseName: tc.Expectation.Trace.DatabaseName, Host: expectTraceHost, PortPathOrID: tc.Expectation.Trace.PortPathOrID, TxnURL: txnURL, Query: query, }}) } } func TestSlowQueryParamsInvalid(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DatastoreTracer.SlowQuery.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "users", Operation: "INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: map[string]interface{}{ "cookies": []string{"chocolate", "sugar", "oatmeal"}, "number": 5, }, } s1.End() app.expectSingleLoggedError(t, "unable to end datastore segment", map[string]interface{}{ "reason": "attribute 'cookies' value of type []string is invalid", }) txn.End() app.ExpectSlowQueries(t, []internal.WantSlowQuery{{ Count: 1, MetricName: "Datastore/statement/MySQL/users/INSERT", Query: "INSERT INTO users (name, age) VALUES ($1, $2)", TxnName: "WebTransaction/Go/hello", TxnURL: "/hello", DatabaseName: "", Host: "", PortPathOrID: "", Params: map[string]interface{}{"number": 5}, }}) } go-agent-3.42.0/v3/newrelic/internal_span_events_test.go000066400000000000000000001364151510742411500232620ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" ) func TestSpanEventSuccess(t *testing.T) { // Test that a basic segment creates a span event, and that a // transaction has a root span event. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := txn.StartSegment("mySegment") segment.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/mySegment", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "e71870997d57214c", "transactionId": "1ae969564b34a33e", "traceId": "1ae969564b34a33ecd1af05fe6923d6d", "parentId": "4259d74b863e2fba", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "priority": internal.MatchAnything, "guid": "4259d74b863e2fba", "transactionId": "1ae969564b34a33e", "nr.entryPoint": true, "traceId": "1ae969564b34a33ecd1af05fe6923d6d", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventsLocallyDisabled(t *testing.T) { // Test that span events do not get created if Config.SpanEvents.Enabled // is false. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := txn.StartSegment("mySegment") segment.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{}) } func TestSpanEventsRemotelyDisabled(t *testing.T) { // Test that span events do not get created if the connect reply // disables span events. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.CollectSpanEvents = false } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := txn.StartSegment("mySegment") segment.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{}) } func TestSpanEventsDisabledWithoutDistributedTracing(t *testing.T) { // Test that span events do not get created distributed tracing is not // enabled. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := txn.StartSegment("mySegment") segment.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{}) } func TestSpanEventDatastoreExternal(t *testing.T) { // Test that a datastore and external segments creates the correct span // events. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.statement": "myquery", "db.instance": "dbname", "db.collection": "mycollection", "peer.address": "myhost:myport", "peer.hostname": "myhost", }, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.url": "http://example.com", "http.method": "GET", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventDatastoreExternalLongDbStatement(t *testing.T) { // Test that a datastore and external segments with a long db statement // creates the correct span events. Should concatenate to 4093 bytes and // add an ellipse to the end replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789AAAAA", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.statement": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789A...", "db.instance": "dbname", "db.collection": "mycollection", "peer.address": "myhost:myport", "peer.hostname": "myhost", }, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.url": "http://example.com", "http.method": "GET", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventDatastoreExternalLongDbInstance(t *testing.T) { // Test that a datastore and external segments with a long db instance // creates the correct span events. Should concatenate to 255 bytes replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHI", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.statement": "myquery", "db.instance": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFG", "db.collection": "mycollection", "peer.address": "myhost:myport", "peer.hostname": "myhost", }, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.url": "http://example.com", "http.method": "GET", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventAttributesDisabled(t *testing.T) { // Test that SpanEvents.Attributes.Enabled correctly disables span // attributes. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventAttributesSpecificallyExcluded(t *testing.T) { // Test that SpanEvents.Attributes.Exclude excludes span attributes. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{ SpanAttributeDBStatement, SpanAttributeDBInstance, SpanAttributeDBCollection, SpanAttributePeerAddress, SpanAttributePeerHostname, SpanAttributeHTTPURL, SpanAttributeHTTPMethod, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventAttributesExcluded(t *testing.T) { // Test that Attributes.Exclude excludes span attributes. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.Attributes.Exclude = []string{ SpanAttributeDBStatement, SpanAttributeDBInstance, SpanAttributeDBCollection, SpanAttributePeerAddress, SpanAttributePeerHostname, SpanAttributeHTTPURL, SpanAttributeHTTPMethod, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanAttributesFromTxnExcludedOnTxn(t *testing.T) { // Test that attributes on the root span that come from transactions do not // get excluded when excluded with TransactionEvents.Attributes.Exclude. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.TransactionEvents.Attributes.Exclude = []string{ AttributeRequestMethod, AttributeRequestURI, AttributeRequestHost, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) txn.SetWebRequestHTTP(req) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "sampled": true, "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.host": "example.com", }, }, }) } func TestSpanAttributesFromTxnExcludedByDefault(t *testing.T) { // Test that the user-agent attribute on span events is excluded by // default but can be included with SpanEvents.Attributes.Include. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) req.Header.Add("User-Agent", "sample user agent") app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(req) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "sampled": true, "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.host": "example.com", }, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.host": "example.com", }, }, }) cfgfn = func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Include = []string{ AttributeRequestUserAgent, } } app = testApp(replyfn, cfgfn, t) txn = app.StartTransaction("hello") txn.SetWebRequestHTTP(req) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "sampled": true, "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.host": "example.com", }, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.userAgent": "sample user agent", "request.headers.host": "example.com", }, }, }) } func TestSpanAttributesFromTxnExcludedOnSpan(t *testing.T) { // Test that attributes on transaction that are shared with the root span // do not get excluded when excluded with SpanEvents.Attributes.Exclude. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{ AttributeRequestMethod, AttributeRequestURI, AttributeRequestHost, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) txn.SetWebRequestHTTP(req) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "sampled": true, "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.method": "GET", "request.uri": "http://example.com", "request.headers.host": "example.com", }, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanAttributesFromTxnExcludedGlobally(t *testing.T) { // Test that attributes on transaction that are shared with the root span // get excluded from both when excluded with Attributes.Exclude. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.Attributes.Exclude = []string{ AttributeRequestMethod, AttributeRequestURI, AttributeRequestHost, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) txn.SetWebRequestHTTP(req) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "sampled": true, "nr.apdexPerfZone": "S", "guid": internal.MatchAnything, "traceId": internal.MatchAnything, "priority": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventAttributesLASP(t *testing.T) { // Test that security policies prevent the capture of the input query // statement. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.SecurityPolicies.RecordSQL.SetEnabled(false) } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") segment := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "mycollection", Operation: "myoperation", ParameterizedQuery: "myquery", Host: "myhost", PortPathOrID: "myport", DatabaseName: "dbname", } segment.End() req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) s := StartExternalSegment(txn, req) s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "sampled": true, "name": "Datastore/statement/MySQL/mycollection/myoperation", "category": "datastore", "component": "MySQL", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.instance": "dbname", "db.collection": "mycollection", "peer.address": "myhost:myport", "peer.hostname": "myhost", "db.statement": "'myoperation' on 'mycollection' using 'MySQL'", }, }, { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/example.com/http/GET", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.url": "http://example.com", "http.method": "GET", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddAgentSpanAttribute(t *testing.T) { // Test that AddAgentSpanAttribute successfully adds attributes to // spans. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") s := txn.StartSegment("hi") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/hi", "sampled": true, "category": "generic", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "aws.operation": "secret", "aws.requestId": "123", "aws.region": "west", }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddAgentSpanAttributeExcluded(t *testing.T) { // Test that span attributes added by AddAgentSpanAttribute are subject // to span attribute configuration. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{ SpanAttributeAWSOperation, SpanAttributeAWSRequestID, SpanAttributeAWSRegion, } } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") s := txn.StartSegment("hi") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/hi", "sampled": true, "category": "generic", "parentId": internal.MatchAnything, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttributeNoActiveSpan(t *testing.T) { // Test that AddAgentSpanAttribute does not have problems if called when // there is no active span. replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") // Do not panic if there are no active spans! internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRegion, "west") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSRequestID, "123") internal.AddAgentSpanAttribute(txn.Private, SpanAttributeAWSOperation, "secret") txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttributeNilTransaction(t *testing.T) { // Test that AddAgentSpanAttribute does not panic if the transaction is // nil. internal.AddAgentSpanAttribute(nil, SpanAttributeAWSRegion, "west") internal.AddAgentSpanAttribute(nil, SpanAttributeAWSRequestID, "123") internal.AddAgentSpanAttribute(nil, SpanAttributeAWSOperation, "secret") } func TestSpanEventHTTPStatusCode(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") resp := &http.Response{ StatusCode: 13, } s := ExternalSegment{ StartTime: txn.StartSegmentNow(), Response: resp, } s.SetStatusCode(0) s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/unknown/http", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ // SetStatusCode takes precedence over Response.StatusCode "http.statusCode": 0, }, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEvent_TxnCustomAttrsAreCopied(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) txn := app.StartTransaction("hello") s := txn.StartSegment("segment") s.End() key := "attr-key" value := "attr-value" txn.AddAttribute(key, value) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "traceId": "52fdfc072182654f163f5f0f9a621d72", "priority": internal.MatchAnything, "guid": "52fdfc072182654f", "sampled": true, }, UserAttributes: map[string]interface{}{ key: value, }, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, // Txn custom attrs should get copied to the root span UserAttributes: map[string]interface{}{ key: value, }, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEvent_TxnCustomAttrsAreExcluded_OnlyFromTxn(t *testing.T) { app := testApp(distributedTracingReplyFields, func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.TransactionEvents.Attributes.Exclude = []string{AttributeRequestMethod} }, t) txn := app.StartTransaction("hello") s := txn.StartSegment("segment") s.End() txn.AddAttribute(AttributeRequestMethod, "attr-value") txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "traceId": "52fdfc072182654f163f5f0f9a621d72", "priority": internal.MatchAnything, "guid": "52fdfc072182654f", "sampled": true, }, // the custom attr should be filtered out UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{ AttributeRequestMethod: "attr-value", }, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEvent_TxnCustomAttrsAreExcluded_OnlyFromSpans(t *testing.T) { app := testApp(distributedTracingReplyFields, func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{AttributeRequestMethod} }, t) txn := app.StartTransaction("hello") s := txn.StartSegment("segment") s.End() txn.AddAttribute(AttributeRequestMethod, "attr-value") txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "traceId": "52fdfc072182654f163f5f0f9a621d72", "priority": internal.MatchAnything, "guid": "52fdfc072182654f", "sampled": true, }, UserAttributes: map[string]interface{}{ AttributeRequestMethod: "attr-value", }, AgentAttributes: map[string]interface{}{}, }, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, // the custom attr should be filtered out UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestSpanEventExcludeCustomAttrs(t *testing.T) { app := testApp(distributedTracingReplyFields, func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{"attribute"} }, t) txn := app.StartTransaction("hello") s := txn.StartSegment("segment") s.AddAttribute("attribute", "value") s.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, // the custom attr should be filtered out UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttributeHighSecurity(t *testing.T) { cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.HighSecurity = true } app := testApp(distributedTracingReplyFields, cfgfn, t) txn := app.StartTransaction("hello") seg := txn.StartSegment("segment") seg.AddAttribute("key", 1) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": errHighSecurityEnabled.Error(), }) seg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, // the custom attr should not be added UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestAddSpanAttributeSecurityPolicyDisablesParameters(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.CustomParameters.SetEnabled(false) } app := testApp(replyfn, enableBetterCAT, t) txn := app.StartTransaction("hello") seg := txn.StartSegment("segment") seg.AddAttribute("key", 1) app.expectSingleLoggedError(t, "unable to add segment attribute", map[string]interface{}{ "reason": errSecurityPolicy.Error(), }) seg.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "Custom/segment", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, // the custom attr should not be added UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } go-agent-3.42.0/v3/newrelic/internal_synthetics_test.go000066400000000000000000000150341510742411500231230ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" ) // This collection of top-level tests affirms, for all possible combinations of // Old CAT, BetterCAT, and Synthetics, that when an inbound request contains a // synthetics header, the subsequent outbound request propagates that synthetics // header. Synthetics uses an obfuscated JSON header, so this test requires a // really particular set of values, e.g. rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr. var ( trustedAccounts = func() map[int]struct{} { ta := make(map[int]struct{}) ta[1] = struct{}{} // Trust account 1, from syntheticsConnectReplyFn. ta[444] = struct{}{} // Trust account 444, from syntheticsHeader. return ta }() syntheticsConnectReplyFn = func(reply *internal.ConnectReply) { reply.EncodingKey = "1234567890123456789012345678901234567890" reply.CrossProcessID = "1#1" reply.TrustedAccounts = trustedAccounts } ) func inboundSyntheticsRequestBuilder(oldCatEnabled bool, betterCatEnabled bool) *http.Request { cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = oldCatEnabled cfg.DistributedTracer.Enabled = betterCatEnabled } app := testApp(syntheticsConnectReplyFn, cfgFn, nil) txn := app.StartTransaction("requester") req, err := http.NewRequest("GET", "newrelic.com", nil) if nil != err { panic(err) } req.Header.Add( "X-NewRelic-Synthetics", "agMfAAECGxpLQkNAQUZHG0VKS0IcAwEHARtFSktCHEBBRkdERUpLQkNAQRYZFF1SU1pbWFkZX1xdUhQBAwEHGV9cXVIUWltYWV5fXF1SU1pbEB8WWFtaVVRdXB9eWVhbGgkLAwUfXllYWxpVVF1cX15ZWFtaVVQSbA==") StartExternalSegment(txn, req) if betterCatEnabled || !oldCatEnabled { if cat.NewRelicIDName == req.Header.Get(cat.NewRelicIDName) { panic("Header contains old cat header NewRelicIDName: " + req.Header.Get(cat.NewRelicIDName)) } if cat.NewRelicTxnName == req.Header.Get(cat.NewRelicTxnName) { panic("Header contains old cat header NewRelicTxnName: " + req.Header.Get(cat.NewRelicTxnName)) } } if oldCatEnabled { if "" == req.Header.Get(cat.NewRelicIDName) { panic("Missing old cat header NewRelicIDName: " + req.Header.Get(cat.NewRelicIDName)) } if "" == req.Header.Get(cat.NewRelicTxnName) { panic("Missing old cat header NewRelicTxnName: " + req.Header.Get(cat.NewRelicTxnName)) } } if "" == req.Header.Get(cat.NewRelicSyntheticsName) { panic("missing synthetics header NewRelicSyntheticsName: " + req.Header.Get(cat.NewRelicSyntheticsName)) } return req } func TestSyntheticsOldCAT(t *testing.T) { cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true cfg.DistributedTracer.Enabled = false } app := testApp(syntheticsConnectReplyFn, cfgFn, t) clientTxn := app.StartTransaction("helloOldCAT") clientTxn.SetWebRequestHTTP(inboundSyntheticsRequestBuilder(true, false)) req, err := http.NewRequest("GET", "newrelic.com", nil) if nil != err { panic(err) } StartExternalSegment(clientTxn, req) clientTxn.End() if "" == req.Header.Get(cat.NewRelicSyntheticsName) { panic("Outbound request missing synthetics header NewRelicSyntheticsName: " + req.Header.Get(cat.NewRelicSyntheticsName)) } expectedIntrinsics := map[string]interface{}{ "name": "WebTransaction/Go/helloOldCAT", "client_cross_process_id": "1#1", "nr.syntheticsResourceId": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "nr.syntheticsJobId": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "nr.syntheticsMonitorId": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm", "nr.apdexPerfZone": internal.MatchAnything, "nr.tripId": internal.MatchAnything, "nr.pathHash": internal.MatchAnything, "nr.referringPathHash": internal.MatchAnything, "nr.referringTransactionGuid": internal.MatchAnything, "nr.guid": internal.MatchAnything, } app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: expectedIntrinsics, }}) } func TestSyntheticsBetterCAT(t *testing.T) { cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = true } app := testApp(syntheticsConnectReplyFn, cfgFn, t) clientTxn := app.StartTransaction("helloBetterCAT") clientTxn.SetWebRequestHTTP(inboundSyntheticsRequestBuilder(false, true)) req, err := http.NewRequest("GET", "newrelic.com", nil) if nil != err { panic(err) } StartExternalSegment(clientTxn, req) clientTxn.End() if "" == req.Header.Get(cat.NewRelicSyntheticsName) { panic("Outbound request missing synthetics header NewRelicSyntheticsName: " + req.Header.Get(cat.NewRelicSyntheticsName)) } expectedIntrinsics := map[string]interface{}{ "name": "WebTransaction/Go/helloBetterCAT", "nr.syntheticsResourceId": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "nr.syntheticsJobId": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "nr.syntheticsMonitorId": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm", "nr.apdexPerfZone": internal.MatchAnything, "priority": internal.MatchAnything, "sampled": internal.MatchAnything, "traceId": internal.MatchAnything, "guid": internal.MatchAnything, } app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: expectedIntrinsics, }}) } func TestSyntheticsStandalone(t *testing.T) { cfgFn := func(cfg *Config) { cfg.AppName = "syntheticsReceiver" cfg.DistributedTracer.Enabled = false cfg.CrossApplicationTracer.Enabled = false } app := testApp(syntheticsConnectReplyFn, cfgFn, t) clientTxn := app.StartTransaction("helloSynthetics") clientTxn.SetWebRequestHTTP(inboundSyntheticsRequestBuilder(false, false)) req, err := http.NewRequest("GET", "newrelic.com", nil) if nil != err { panic(err) } StartExternalSegment(clientTxn, req) clientTxn.End() if "" == req.Header.Get(cat.NewRelicSyntheticsName) { panic("Outbound request missing synthetics header NewRelicSyntheticsName: " + req.Header.Get(cat.NewRelicSyntheticsName)) } expectedIntrinsics := map[string]interface{}{ "name": "WebTransaction/Go/helloSynthetics", "nr.syntheticsResourceId": "rrrrrrr-rrrr-1234-rrrr-rrrrrrrrrrrr", "nr.syntheticsJobId": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "nr.syntheticsMonitorId": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm", "nr.apdexPerfZone": internal.MatchAnything, "nr.guid": internal.MatchAnything, } app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: expectedIntrinsics, }}) } go-agent-3.42.0/v3/newrelic/internal_test.go000066400000000000000000002243351510742411500206540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "errors" "fmt" "math" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) var ( singleCount = []float64{1, 0, 0, 0, 0, 0, 0} webMetrics = []internal.WantMetric{ {Name: "WebTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, {Name: "WebTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, {Name: "Apdex", Scope: "", Forced: true, Data: nil}, {Name: "Apdex/Go/hello", Scope: "", Forced: false, Data: nil}, } webErrorMetrics = append([]internal.WantMetric{ {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/WebTransaction/Go/hello", Scope: "", Forced: true, Data: singleCount}, }, webMetrics...) backgroundMetrics = []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, } backgroundMetricsUnknownCaller = append([]internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }, backgroundMetrics...) backgroundErrorMetrics = append([]internal.WantMetric{ {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/allOther", Scope: "", Forced: true, Data: singleCount}, {Name: "Errors/OtherTransaction/Go/hello", Scope: "", Forced: true, Data: singleCount}, }, backgroundMetrics...) backgroundErrorMetricsUnknownCaller = append([]internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, }, backgroundErrorMetrics...) ) type recordedLogMessage struct { msg string context map[string]interface{} } type errorSaverLogger struct { sync.Mutex errors []recordedLogMessage } func (lg *errorSaverLogger) expectNoLoggedErrors(tb testing.TB) { if h, ok := tb.(interface { Helper() }); ok { h.Helper() } if len(lg.errors) != 0 { tb.Error("unexpected non-zero number of errors logged", len(lg.errors)) } } func (lg *errorSaverLogger) expectSingleLoggedError(tb testing.TB, msg string, context map[string]interface{}) { if h, ok := tb.(interface { Helper() }); ok { h.Helper() } lg.Lock() errs := lg.errors lg.Unlock() if len(errs) != 1 { tb.Error("unexpected number of errors logged", len(errs)) return } if errs[0].msg != msg { tb.Error("incorrect logged error message", errs[0].msg, msg) return } for k, v := range context { var fail bool switch val := v.(type) { case string: // If the value is type string, then only assert that the actual // value contains the expected value rather than them being equal. fail = !strings.Contains(errs[0].context[k].(string), val) default: fail = errs[0].context[k] != val } if fail { tb.Error("incorrect logged error context", errs[0].context, context) } } // Reset to prepare for subsequent tests. lg.Lock() lg.errors = nil lg.Unlock() } func (lg *errorSaverLogger) Error(msg string, context map[string]interface{}) { lg.Lock() defer lg.Unlock() lg.errors = append(lg.errors, recordedLogMessage{msg: msg, context: context}) } func (lg *errorSaverLogger) Warn(msg string, context map[string]interface{}) {} func (lg *errorSaverLogger) Info(msg string, context map[string]interface{}) {} func (lg *errorSaverLogger) Debug(msg string, context map[string]interface{}) {} func (lg *errorSaverLogger) DebugEnabled() bool { return false } // compatibleResponseRecorder wraps ResponseRecorder to ensure consistent behavior // between different versions of Go. // // Unfortunately, there was a behavior change in go1.6: // // "The net/http/httptest package's ResponseRecorder now initializes a default // Content-Type header using the same content-sniffing algorithm as in // http.Server." type compatibleResponseRecorder struct { *httptest.ResponseRecorder wroteHeader bool } func newCompatibleResponseRecorder() *compatibleResponseRecorder { recorder := compatibleResponseRecorder{ ResponseRecorder: httptest.NewRecorder(), } return &recorder } func (rw *compatibleResponseRecorder) Header() http.Header { return rw.ResponseRecorder.Header() } func (rw *compatibleResponseRecorder) Write(buf []byte) (int, error) { if !rw.wroteHeader { rw.WriteHeader(200) rw.wroteHeader = true } return rw.ResponseRecorder.Write(buf) } func (rw *compatibleResponseRecorder) WriteHeader(code int) { rw.wroteHeader = true rw.ResponseRecorder.WriteHeader(code) } var ( validParams = map[string]interface{}{"zip": 1, "zap": 2} ) var ( helloResponse = []byte("hello") helloPath = "/hello" helloQueryParams = "?secret=hideme" helloRequest = func() *http.Request { r, err := http.NewRequest("GET", helloPath+helloQueryParams, nil) if nil != err { panic(err) } r.Header.Add(`Accept`, `text/plain`) r.Header.Add(`Content-Type`, `text/html; charset=utf-8`) r.Header.Add(`Content-Length`, `753`) r.Header.Add(`User-Agent`, `Mozilla/5.0`) r.Header.Add(`Referer`, `http://en.wikipedia.org/zip?secret=password`) //we should pull the host from the request field, not the headers r.Header.Add(`Host`, `wrongHost`) r.Host = "my_domain.com" return r }() helloRequestAttributes = map[string]interface{}{ "request.uri": "/hello", "request.headers.host": "my_domain.com", "request.headers.referer": "http://en.wikipedia.org/zip", "request.headers.contentLength": 753, "request.method": "GET", "request.headers.accept": "text/plain", "request.headers.User-Agent": "Mozilla/5.0", "request.headers.userAgent": "Mozilla/5.0", "request.headers.contentType": "text/html; charset=utf-8", } ) func TestNewApplicationNil(t *testing.T) { app, err := NewApplication( ConfigAppName("appname"), ConfigLicense("wrong length"), ConfigEnabled(false), ConfigCodeLevelMetricsEnabled(false), ) if nil == err { t.Error("error expected when license key is short") } if nil != app { t.Error("app expected to be nil when error is returned") } } func handler(w http.ResponseWriter, req *http.Request) { w.Write(helloResponse) } const ( testLicenseKey = "0123456789012345678901234567890123456789" ) type expectApp struct { *Application internal.Expect *errorSaverLogger } func (ea expectApp) ExpectCustomEvents(t internal.Validator, want []internal.WantEvent) { ea.Application.Private.(internal.Expect).ExpectCustomEvents(t, want) } func (ea expectApp) ExpectErrors(t internal.Validator, want []internal.WantError) { ea.Application.Private.(internal.Expect).ExpectErrors(t, want) } func (ea expectApp) ExpectErrorEvents(t internal.Validator, want []internal.WantEvent) { ea.Application.Private.(internal.Expect).ExpectErrorEvents(t, want) } func (ea expectApp) ExpectTxnEvents(t internal.Validator, want []internal.WantEvent) { ea.Application.Private.(internal.Expect).ExpectTxnEvents(t, want) } func (ea expectApp) ExpectMetrics(t internal.Validator, want []internal.WantMetric) { ea.Application.Private.(internal.Expect).ExpectMetrics(t, want) } func (ea expectApp) ExpectMetricsPresent(t internal.Validator, want []internal.WantMetric) { ea.Application.Private.(internal.Expect).ExpectMetricsPresent(t, want) } func (ea expectApp) ExpectTxnMetrics(t internal.Validator, want internal.WantTxn) { ea.Application.Private.(internal.Expect).ExpectTxnMetrics(t, want) } func (ea expectApp) ExpectTxnTraces(t internal.Validator, want []internal.WantTxnTrace) { ea.Application.Private.(internal.Expect).ExpectTxnTraces(t, want) } func (ea expectApp) ExpectSlowQueries(t internal.Validator, want []internal.WantSlowQuery) { ea.Application.Private.(internal.Expect).ExpectSlowQueries(t, want) } func (ea expectApp) ExpectSpanEvents(t internal.Validator, want []internal.WantEvent) { ea.Application.Private.(internal.Expect).ExpectSpanEvents(t, want) } func testApp(replyfn func(*internal.ConnectReply), cfgfn func(*Config), t testing.TB) expectApp { lg := &errorSaverLogger{} app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(testLicenseKey), ConfigCodeLevelMetricsEnabled(false), cfgfn, func(cfg *Config) { cfg.Logger = lg // Prevent spawning app goroutines in tests. if !cfg.ServerlessMode.Enabled { cfg.Enabled = false } }, ) if nil != err { t.Fatal(err) } internal.HarvestTesting(app.Private, replyfn) return expectApp{ Application: app, errorSaverLogger: lg, } } func TestRecordLLMFeedbackEventSuccess(t *testing.T) { app := testApp(nil, nil, t) app.RecordLLMFeedbackEvent("traceid", "5", "informative", "message", validParams) app.expectNoLoggedErrors(t) app.ExpectCustomEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "LlmFeedbackMessage", "timestamp": internal.MatchAnything, }, UserAttributes: map[string]interface{}{ "trace_id": "traceid", "rating": "5", "category": "informative", "message": "message", "ingest_source": "Go", "zip": 1, "zap": 2, }, }}) } func TestRecordCustomEventSuccess(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomEvent("myType", validParams) app.expectNoLoggedErrors(t) app.ExpectCustomEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "type": "myType", "timestamp": internal.MatchAnything, }, UserAttributes: validParams, }}) } func TestRecordCustomEventHighSecurityEnabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.HighSecurity = true } app := testApp(nil, cfgfn, t) app.RecordCustomEvent("myType", validParams) app.expectSingleLoggedError(t, "unable to record custom event", map[string]interface{}{ "event-type": "myType", "reason": errHighSecurityEnabled.Error(), }) app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestRecordCustomEventSecurityPolicy(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SecurityPolicies.CustomEvents.SetEnabled(false) } app := testApp(replyfn, nil, t) app.RecordCustomEvent("myType", validParams) app.expectSingleLoggedError(t, "unable to record custom event", map[string]interface{}{ "event-type": "myType", "reason": errSecurityPolicy.Error(), }) app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestRecordCustomEventEventsDisabled(t *testing.T) { cfgfn := func(cfg *Config) { cfg.CustomInsightsEvents.Enabled = false } app := testApp(nil, cfgfn, t) app.RecordCustomEvent("myType", validParams) app.expectSingleLoggedError(t, "unable to record custom event", map[string]interface{}{ "event-type": "myType", "reason": errCustomEventsDisabled.Error(), }) app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestRecordCustomEventBadInput(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomEvent("????", validParams) app.expectSingleLoggedError(t, "unable to record custom event", map[string]interface{}{ "event-type": "????", "reason": errEventTypeRegex.Error(), }) app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestRecordCustomEventRemoteDisable(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.CollectCustomEvents = false } app := testApp(replyfn, nil, t) app.RecordCustomEvent("myType", validParams) app.expectSingleLoggedError(t, "unable to record custom event", map[string]interface{}{ "event-type": "myType", "reason": errCustomEventsRemoteDisabled.Error(), }) app.ExpectCustomEvents(t, []internal.WantEvent{}) } func TestRecordCustomMetricSuccess(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomMetric("myMetric", 123.0) app.expectNoLoggedErrors(t) expectData := []float64{1, 123.0, 123.0, 123.0, 123.0, 123.0 * 123.0} app.ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/myMetric", Scope: "", Forced: false, Data: expectData}, }) } func TestRecordCustomMetricNameEmpty(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomMetric("", 123.0) app.expectSingleLoggedError(t, "unable to record custom metric", map[string]interface{}{ "metric-name": "", "reason": errMetricNameEmpty.Error(), }) } func TestRecordCustomMetricNaN(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomMetric("myMetric", math.NaN()) app.expectSingleLoggedError(t, "unable to record custom metric", map[string]interface{}{ "metric-name": "myMetric", "reason": errMetricNaN.Error(), }) } func TestRecordCustomMetricPositiveInf(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomMetric("myMetric", math.Inf(0)) app.expectSingleLoggedError(t, "unable to record custom metric", map[string]interface{}{ "metric-name": "myMetric", "reason": errMetricInf.Error(), }) } func TestRecordCustomMetricNegativeInf(t *testing.T) { app := testApp(nil, nil, t) app.RecordCustomMetric("myMetric", math.Inf(-1)) app.expectSingleLoggedError(t, "unable to record custom metric", map[string]interface{}{ "metric-name": "myMetric", "reason": errMetricInf.Error(), }) } type sampleResponseWriter struct { code int written int header http.Header } func (w *sampleResponseWriter) Header() http.Header { return w.header } func (w *sampleResponseWriter) Write([]byte) (int, error) { return w.written, nil } func (w *sampleResponseWriter) WriteHeader(x int) { w.code = x } func TestTxnResponseWriter(t *testing.T) { // NOTE: Eventually when the ResponseWriter is instrumented, this test // should be expanded to make sure that calling ResponseWriter methods // after the transaction has ended is not problematic. w := &sampleResponseWriter{ header: make(http.Header), } app := testApp(nil, nil, t) txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) w.header.Add("zip", "zap") if out := rw.Header(); out.Get("zip") != "zap" { t.Error(out.Get("zip")) } w.written = 123 if out, _ := rw.Write(nil); out != 123 { t.Error(out) } if rw.WriteHeader(503); w.code != 503 { t.Error(w.code) } } func TestTransactionEventWeb(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.expectNoLoggedErrors(t) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "S", }, }}) } func TestTransactionEventBackground(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.End() app.expectNoLoggedErrors(t) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, }}) } func TestTransactionEventLocallyDisabled(t *testing.T) { cfgFn := func(cfg *Config) { cfg.TransactionEvents.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.expectNoLoggedErrors(t) app.ExpectTxnEvents(t, []internal.WantEvent{}) } func TestTransactionEventRemotelyDisabled(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.CollectAnalyticsEvents = false } app := testApp(replyfn, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.expectNoLoggedErrors(t) app.ExpectTxnEvents(t, []internal.WantEvent{}) } func TestSetName(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("one") txn.SetName("hello") txn.End() app.expectNoLoggedErrors(t) txn.SetName("three") app.expectSingleLoggedError(t, "unable to set transaction name", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, backgroundMetrics) } type advancedError struct { error } func (e *advancedError) Error() string { return e.error.Error() } func (e *advancedError) ErrorClass() string { return "test class" } func (e *advancedError) ErrorAttributes() map[string]interface{} { return map[string]interface{}{ "testKey": "test val", } } func TestErrorWithCallback(t *testing.T) { errorGroupFunc := func(e ErrorInfo) string { if e.Error == nil { t.Error("expected ErrorInfo.Error not be nil") } if e.Expected { t.Error("error should not be expected") } val, ok := e.GetErrorAttribute("testKey") if !ok || val != "test val" { t.Error("error should successfully look up user provided attribute: \"testKey\":\"test val\"") } val, ok = e.GetTransactionUserAttribute("txnAttribute") if !ok || val != "test txn attr" { t.Error("error should successfully look up user provided attribute: \"testKey\":\"test txn attr\"") } stackTrace := e.GetStackTraceFrames() if len(stackTrace) == 0 { t.Error("expected error stack trace to not be empty") } AssertStringEqual(t, "ErrorInfo.TransactionName", `OtherTransaction/Go/hello`, e.TransactionName) AssertStringEqual(t, "ErrorInfo.Message", "this is a test error", e.Message) AssertStringEqual(t, "ErrorInfo.Class", "test class", e.Class) return "testGroup" } app := testApp( nil, func(cfg *Config) { cfg.DistributedTracer.Enabled = false enableRecordPanics(cfg) cfg.ErrorCollector.ErrorGroupCallback = errorGroupFunc }, t, ) txn := app.StartTransaction("hello") txn.AddAttribute("txnAttribute", "test txn attr") txn.NoticeError(&advancedError{errors.New("this is a test error")}) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "this is a test error", Klass: "test class", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "test class", "error.message": "this is a test error", "transactionName": "OtherTransaction/Go/hello", }, }}) } func deferEndPanic(txn *Transaction, panicMe interface{}) (r interface{}) { defer func() { r = recover() }() defer txn.End() panic(panicMe) } func enableRecordPanics(cfg *Config) { cfg.ErrorCollector.RecordPanics = true } func TestPanicNotEnabled(t *testing.T) { // Test that panics are not recorded as errors if the config setting has // not been enabled. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") e := myError{} r := deferEndPanic(txn, e) if r != e { t.Error("panic not propagated", r) } app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } func TestPanicError(t *testing.T) { app := testApp(nil, func(cfg *Config) { enableRecordPanics(cfg) cfg.DistributedTracer.Enabled = false }, t) txn := app.StartTransaction("hello") e := myError{} r := deferEndPanic(txn, e) if r != e { t.Error("panic not propagated", r) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: panicErrorKlass, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": panicErrorKlass, "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestPanicErrorWithCallback(t *testing.T) { errorGroupFunc := func(e ErrorInfo) string { if e.Error != nil { t.Errorf("expected ErrorInfo.Error to be nil, but got %v", e.Error) } AssertStringEqual(t, "ErrorInfo.TransactionName", `OtherTransaction/Go/hello`, e.TransactionName) AssertStringEqual(t, "ErrorInfo.Message", "my msg", e.Message) AssertStringEqual(t, "ErrorInfo.Class", PanicErrorClass, e.Class) return "testGroup" } app := testApp( nil, func(cfg *Config) { cfg.DistributedTracer.Enabled = false enableRecordPanics(cfg) cfg.ErrorCollector.ErrorGroupCallback = errorGroupFunc }, t, ) txn := app.StartTransaction("hello") e := myError{} r := deferEndPanic(txn, e) if r != e { t.Error("panic not propagated", r) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: panicErrorKlass, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": panicErrorKlass, "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestPanicString(t *testing.T) { app := testApp(nil, func(cfg *Config) { enableRecordPanics(cfg) cfg.DistributedTracer.Enabled = false }, t) txn := app.StartTransaction("hello") e := "my string" r := deferEndPanic(txn, e) if r != e { t.Error("panic not propagated", r) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my string", Klass: panicErrorKlass, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": panicErrorKlass, "error.message": "my string", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } func TestPanicInt(t *testing.T) { app := testApp(nil, func(cfg *Config) { enableRecordPanics(cfg) cfg.DistributedTracer.Enabled = false }, t) txn := app.StartTransaction("hello") e := 22 r := deferEndPanic(txn, e) if r != e { t.Error("panic not propagated", r) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "22", Klass: panicErrorKlass, }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": panicErrorKlass, "error.message": "22", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) } /* superseded now by TestPanicNilRecovery. func TestPanicNil(t *testing.T) { app := testApp(nil, func(cfg *Config) { enableRecordPanics(cfg) cfg.DistributedTracer.Enabled = false }, t) txn := app.StartTransaction("hello") r := deferEndPanic(txn, nil) if nil != r { t.Error(r) } app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, backgroundMetrics) } */ func TestResponseCodeError(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(http.StatusBadRequest) // 400 rw.WriteHeader(http.StatusUnauthorized) // 401 txn.End() if http.StatusBadRequest != w.Code { t.Error(w.Code) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "Bad Request", Klass: "400", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "400", "error.message": "Bad Request", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: mergeAttributes(helloRequestAttributes, map[string]interface{}{ "httpResponseCode": "400", "http.statusCode": "400", }), }}) app.ExpectMetrics(t, webErrorMetrics) } func AssertStringEqual(t *testing.T, field string, expect string, actual string) { if expect != actual { t.Errorf("incorrect value for %s; expected: %s got: %s", field, expect, actual) } } func TestResponseCodeErrorWithCallback(t *testing.T) { errorGroupFunc := func(e ErrorInfo) string { if e.Error != nil { t.Errorf("expected ErrorInfo.Error to be nil, but got %v", e.Error) } AssertStringEqual(t, "ErrorInfo.TransactionName", `WebTransaction/Go/hello`, e.TransactionName) AssertStringEqual(t, "ErrorInfo.Message", "Bad Request", e.Message) AssertStringEqual(t, "ErrorInfo.Class", "400", e.Class) val, ok := e.GetTransactionUserAttribute("test") if !ok { t.Errorf("expected attribute \"test\" to be found in txn attributes") } else { AssertStringEqual(t, "User Txn Attribute \"test\"", "test value", fmt.Sprint(val)) } return "testGroup" } app := testApp( nil, func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.ErrorCollector.ErrorGroupCallback = errorGroupFunc }, t, ) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") txn.AddAttribute("test", "test value") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(http.StatusBadRequest) // 400 rw.WriteHeader(http.StatusUnauthorized) // 401 txn.End() if http.StatusBadRequest != w.Code { t.Error(w.Code) } app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "Bad Request", Klass: "400", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "400", "error.message": "Bad Request", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: mergeAttributes(helloRequestAttributes, map[string]interface{}{ "httpResponseCode": "400", "http.statusCode": "400", AttributeErrorGroupName: "testGroup", }), }}) app.ExpectMetrics(t, webErrorMetrics) } func TestErrorGroupCallbackWithHighSecurity(t *testing.T) { errorGroupFunc := func(e ErrorInfo) string { if e.Error != nil { t.Errorf("expected ErrorInfo.Error to be nil, but got %v", e.Error) } AssertStringEqual(t, "ErrorInfo.TransactionName", `WebTransaction/Go/hello`, e.TransactionName) AssertStringEqual(t, "ErrorInfo.Message", "Bad Request", e.Message) AssertStringEqual(t, "ErrorInfo.Class", "400", e.Class) AssertStringEqual(t, "Request URI", "/hello", e.GetRequestURI()) AssertStringEqual(t, "Request Method", "GET", e.GetRequestMethod()) AssertStringEqual(t, "Response Code", "400", e.GetHttpResponseCode()) _, ok := e.GetTransactionUserAttribute("test") if ok { t.Errorf("attributes can not be recorded during high security mode") } return "testGroup" } app := testApp( nil, func(cfg *Config) { cfg.DistributedTracer.Enabled = false cfg.ErrorCollector.ErrorGroupCallback = errorGroupFunc cfg.DistributedTracer.Enabled = true cfg.HighSecurity = true }, t, ) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") // you may not record user attributes with high security enabled txn.AddAttribute("test", "test value") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(http.StatusBadRequest) // 400 rw.WriteHeader(http.StatusUnauthorized) // 401 txn.End() } func TestResponseCode404Filtered(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(http.StatusNotFound) txn.End() if http.StatusNotFound != w.Code { t.Error(w.Code) } app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, webMetrics) } func TestResponseCodeCustomFilter(t *testing.T) { cfgFn := func(cfg *Config) { cfg.ErrorCollector.IgnoreStatusCodes = []int{405} cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(405) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, webMetrics) } func TestResponseCodeServerSideFilterObserved(t *testing.T) { // Test that server-side ignore_status_codes are observed. cfgFn := func(cfg *Config) { cfg.ErrorCollector.IgnoreStatusCodes = nil cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"error_collector.ignore_status_codes":[405]}}`), reply) } app := testApp(replyfn, cfgFn, t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(405) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, webMetrics) } func TestResponseCodeServerSideOverwriteLocal(t *testing.T) { // Test that server-side ignore_status_codes are used in place of local // Config.ErrorCollector.IgnoreStatusCodes. cfgFn := func(cfg *Config) { cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"error_collector.ignore_status_codes":[402]}}`), reply) } app := testApp(replyfn, cfgFn, t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.WriteHeader(404) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "Not Found", Klass: "404", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "404", "error.message": "Not Found", "transactionName": "WebTransaction/Go/hello", }, AgentAttributes: mergeAttributes(helloRequestAttributes, map[string]interface{}{ "httpResponseCode": "404", "http.statusCode": "404", }), }}) app.ExpectMetrics(t, webErrorMetrics) } func TestResponseCodeAfterEnd(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) txn.End() rw.WriteHeader(http.StatusBadRequest) if http.StatusBadRequest != w.Code { t.Error(w.Code) } app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, webMetrics) } func TestResponseCodeAfterWrite(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) w := newCompatibleResponseRecorder() txn := app.StartTransaction("hello") rw := txn.SetWebResponse(w) txn.SetWebRequestHTTP(helloRequest) rw.Write([]byte("zap")) rw.WriteHeader(http.StatusBadRequest) txn.End() if out := w.Body.String(); out != "zap" { t.Error(out) } if http.StatusOK != w.Code { t.Error(w.Code) } app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, webMetrics) } func TestQueueTime(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) req, err := http.NewRequest("GET", helloPath+helloQueryParams, nil) req.Header.Add("X-Queue-Start", "1465793282.12345") if nil != err { t.Fatal(err) } txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(req) txn.NoticeError(myError{}) txn.End() app.ExpectErrors(t, []internal.WantError{{ TxnName: "WebTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", "queueDuration": internal.MatchAnything, }, AgentAttributes: map[string]interface{}{ "request.uri": "/hello", "request.method": "GET", }, }}) app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "WebFrontend/QueueTime", Scope: "", Forced: true, Data: nil}, }, webErrorMetrics...)) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", "queueDuration": internal.MatchAnything, }, AgentAttributes: nil, }}) } func TestIgnore(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) txn.Ignore() app.expectNoLoggedErrors(t) txn.End() app.ExpectErrors(t, []internal.WantError{}) app.ExpectErrorEvents(t, []internal.WantEvent{}) app.ExpectMetrics(t, []internal.WantMetric{}) app.ExpectTxnEvents(t, []internal.WantEvent{}) } func TestIgnoreAlreadyEnded(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) txn.End() txn.Ignore() app.expectSingleLoggedError(t, "unable to ignore transaction", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectErrors(t, []internal.WantError{{ TxnName: "OtherTransaction/Go/hello", Msg: "my msg", Klass: "newrelic.myError", }}) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", }, }}) app.ExpectMetrics(t, backgroundErrorMetrics) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, }}) } func TestExternalSegmentMethod(t *testing.T) { req, err := http.NewRequest("POST", "http://request.com/", nil) if err != nil { t.Fatal(err) } responsereq, err := http.NewRequest("POST", "http://response.com/", nil) if err != nil { t.Fatal(err) } response := &http.Response{Request: responsereq} // empty segment m := externalSegmentMethod(&ExternalSegment{}) if "" != m { t.Error(m) } // empty request m = externalSegmentMethod(&ExternalSegment{ Request: nil, }) if "" != m { t.Error(m) } // segment containing request and response m = externalSegmentMethod(&ExternalSegment{ Request: req, Response: response, }) if "POST" != m { t.Error(m) } // Procedure field overrides request and response. m = externalSegmentMethod(&ExternalSegment{ Procedure: "GET", Request: req, Response: response, }) if "GET" != m { t.Error(m) } req, err = http.NewRequest("", "http://request.com/", nil) if err != nil { t.Fatal(err) } responsereq, err = http.NewRequest("", "http://response.com/", nil) if err != nil { t.Fatal(err) } response = &http.Response{Request: responsereq} // empty string method means a client GET request m = externalSegmentMethod(&ExternalSegment{ Request: req, Response: response, }) if "GET" != m { t.Error(m) } } func TestExternalSegmentURL(t *testing.T) { rawURL := "http://url.com" req, err := http.NewRequest("GET", "http://request.com/", nil) if err != nil { t.Fatal(err) } responsereq, err := http.NewRequest("GET", "http://response.com/", nil) if err != nil { t.Fatal(err) } response := &http.Response{Request: responsereq} // empty segment u, err := externalSegmentURL(&ExternalSegment{}) host := hostFromURL(u) if nil != err || nil != u || "" != host { t.Error(u, err, hostFromURL(u)) } // segment only containing url u, err = externalSegmentURL(&ExternalSegment{URL: rawURL}) host = hostFromURL(u) if nil != err || host != "url.com" { t.Error(u, err, hostFromURL(u)) } // segment only containing request u, err = externalSegmentURL(&ExternalSegment{Request: req}) host = hostFromURL(u) if nil != err || "request.com" != host { t.Error(host) } // segment only containing response u, err = externalSegmentURL(&ExternalSegment{Response: response}) host = hostFromURL(u) if nil != err || "response.com" != host { t.Error(host) } // segment containing request and response u, err = externalSegmentURL(&ExternalSegment{ Request: req, Response: response, }) host = hostFromURL(u) if nil != err || "response.com" != host { t.Error(host) } // segment containing url, request, and response u, err = externalSegmentURL(&ExternalSegment{ URL: rawURL, Request: req, Response: response, }) host = hostFromURL(u) if nil != err || "url.com" != host { t.Error(err, host) } } func TestZeroSegmentsSafe(t *testing.T) { s := Segment{} s.End() StartSegmentNow(nil) ds := DatastoreSegment{} ds.End() es := ExternalSegment{} es.End() StartSegment(nil, "").End() StartExternalSegment(nil, nil).End() } func TestTraceSegmentDefer(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) func() { defer txn.StartSegment("segment").End() }() txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Custom/segment", Scope: "", Forced: false, Data: nil}, {Name: "Custom/segment", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestTraceSegmentNilErr(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.StartSegment("segment").End() app.expectNoLoggedErrors(t) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Custom/segment", Scope: "", Forced: false, Data: nil}, {Name: "Custom/segment", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestTraceSegmentOutOfOrder(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := txn.StartSegment("s1") s2 := txn.StartSegment("s1") s1.End() app.expectNoLoggedErrors(t) s2.End() app.expectSingleLoggedError(t, "unable to end segment", map[string]interface{}{ "reason": errSegmentOrder.Error(), }) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Custom/s1", Scope: "", Forced: false, Data: nil}, {Name: "Custom/s1", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestTraceSegmentEndedBeforeStartSegment(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() s := txn.StartSegment("segment") s.End() app.expectSingleLoggedError(t, "unable to end segment", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, webMetrics) } func TestTraceSegmentEndedBeforeEndSegment(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := txn.StartSegment("segment") txn.End() s.End() app.expectSingleLoggedError(t, "unable to end segment", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, webMetrics) } func TestTraceSegmentPanic(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) func() { defer func() { recover() }() func() { defer txn.StartSegment("f1").End() func() { t := txn.StartSegment("f2") func() { defer txn.StartSegment("f3").End() func() { txn.StartSegment("f4") panic(nil) }() }() t.End() }() }() }() txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Custom/f1", Scope: "", Forced: false, Data: nil}, {Name: "Custom/f1", Scope: scope, Forced: false, Data: nil}, {Name: "Custom/f3", Scope: "", Forced: false, Data: nil}, {Name: "Custom/f3", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) } func TestTraceSegmentNilTxn(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := Segment{Name: "hello"} s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, webMetrics) } func TestTraceDatastore(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := DatastoreSegment{} s.StartTime = txn.StartSegmentNow() s.Product = DatastoreMySQL s.Collection = "my_table" s.Operation = "SELECT" s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/SELECT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: scope, Forced: false, Data: nil}, }, webErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) } func TestTraceDatastoreBackground(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "SELECT", } s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "OtherTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/SELECT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: scope, Forced: false, Data: nil}, }, backgroundErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) } func TestTraceDatastoreMissingProductOperationCollection(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := DatastoreSegment{ StartTime: txn.StartSegmentNow(), } s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/Unknown/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/Unknown/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/Unknown/other", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/operation/Unknown/other", Scope: scope, Forced: false, Data: nil}, }, webErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", "databaseCallCount": 1, "databaseDuration": internal.MatchAnything, }, }}) } func TestTraceDatastoreNilTxn(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) var s DatastoreSegment s.Product = DatastoreMySQL s.Collection = "my_table" s.Operation = "SELECT" s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() app.ExpectMetrics(t, webErrorMetrics) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, }}) } func TestTraceDatastoreTxnEnded(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.NoticeError(myError{}) s := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "SELECT", } txn.End() s.End() app.expectSingleLoggedError(t, "unable to end datastore segment", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, webErrorMetrics) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, }}) } func TestTraceExternal(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com/", } s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http", Scope: scope, Forced: false, Data: nil}, }, webErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) } func TestExternalSegmentCustomFieldsWithURL(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "https://otherhost.com/path/zip/zap?secret=ssshhh", Host: "bufnet", Procedure: "TestApplication/DoUnaryUnary", Library: "grpc", } s.End() app.expectNoLoggedErrors(t) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/grpc/TestApplication/DoUnaryUnary", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/bufnet/grpc/TestApplication/DoUnaryUnary", "category": "http", "component": "grpc", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ // "http.url" and "http.method" are not saved if // library is not "http". }, }, { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.uri": "/hello", "request.headers.contentType": "text/html; charset=utf-8", "request.headers.host": "my_domain.com", "request.method": "GET", "request.headers.contentLength": 753, "request.headers.accept": "text/plain", }, }, }) } func TestExternalSegmentCustomFieldsWithRequest(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) req, _ := http.NewRequest("GET", "https://www.something.com/path/zip/zap?secret=ssshhh", nil) s := StartExternalSegment(txn, req) s.Host = "bufnet" s.Procedure = "TestApplication/DoUnaryUnary" s.Library = "grpc" s.End() app.expectNoLoggedErrors(t) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/grpc/TestApplication/DoUnaryUnary", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/bufnet/grpc/TestApplication/DoUnaryUnary", "category": "http", "component": "grpc", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ // "http.url" and "http.method" are not saved if // library is not "http". }, }, { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.uri": "/hello", "request.headers.contentType": "text/html; charset=utf-8", "request.headers.host": "my_domain.com", "request.method": "GET", "request.headers.contentLength": 753, "request.headers.accept": "text/plain", }, }, }) } func TestExternalSegmentCustomFieldsWithResponse(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.CrossApplicationTracer.Enabled = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) req, _ := http.NewRequest("GET", "https://www.something.com/path/zip/zap?secret=ssshhh", nil) resp := &http.Response{ Request: req, StatusCode: 13, } s := ExternalSegment{ StartTime: txn.StartSegmentNow(), Response: resp, Host: "bufnet", Procedure: "TestApplication/DoUnaryUnary", Library: "grpc", } s.End() app.expectNoLoggedErrors(t) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "External/bufnet/all", Scope: "", Forced: false, Data: nil}, {Name: "External/bufnet/grpc/TestApplication/DoUnaryUnary", Scope: scope, Forced: false, Data: nil}, }, webMetrics...)) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "External/bufnet/grpc/TestApplication/DoUnaryUnary", "category": "http", "component": "grpc", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ // "http.url" and "http.method" are not saved if // library is not "http". "http.statusCode": 13, }, }, { Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "transaction.name": "WebTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "request.uri": "/hello", "request.headers.contentType": "text/html; charset=utf-8", "request.headers.host": "my_domain.com", "request.method": "GET", "request.headers.contentLength": 753, "request.headers.accept": "text/plain", }, }, }) } func TestTraceExternalBadURL(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: ":example.com/", } s.End() app.expectSingleLoggedError(t, "unable to end external segment", map[string]interface{}{ "reason": "missing protocol scheme", }) txn.NoticeError(myError{}) txn.End() app.ExpectMetrics(t, webErrorMetrics) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, }}) } func TestTraceExternalBackground(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com/", } s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "OtherTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, {Name: "External/example.com/all", Scope: "", Forced: false, Data: nil}, {Name: "External/example.com/http", Scope: scope, Forced: false, Data: nil}, }, backgroundErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) } func TestTraceExternalMissingURL(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s := ExternalSegment{ StartTime: txn.StartSegmentNow(), } s.End() app.expectNoLoggedErrors(t) txn.NoticeError(myError{}) txn.End() scope := "WebTransaction/Go/hello" app.ExpectMetrics(t, append([]internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: nil}, {Name: "External/allWeb", Scope: "", Forced: true, Data: nil}, {Name: "External/unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "External/unknown/http", Scope: scope, Forced: false, Data: nil}, }, webErrorMetrics...)) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", "externalCallCount": 1, "externalDuration": internal.MatchAnything, }, }}) } func TestTraceExternalNilTxn(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.NoticeError(myError{}) var s ExternalSegment s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, webErrorMetrics) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, }}) } func TestTraceExternalTxnEnded(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.NoticeError(myError{}) s := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com/", } txn.End() s.End() app.expectSingleLoggedError(t, "unable to end external segment", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, webErrorMetrics) app.ExpectErrorEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "error.class": "newrelic.myError", "error.message": "my msg", "transactionName": "WebTransaction/Go/hello", }, }}) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "WebTransaction/Go/hello", "nr.apdexPerfZone": "F", }, }}) } func TestTraceBelowThreshold(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceBelowThresholdBackground(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceNoSegments(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, }}) } func TestTraceDisabledLocally(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Enabled = false cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceDisabledByServerSideConfig(t *testing.T) { // Test that server-side-config trace-enabled-setting can disable transaction // traces. cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.enabled":false}}`), reply) } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceEnabledByServerSideConfig(t *testing.T) { // Test that server-side-config trace-enabled-setting can enable // transaction traces (and hence server-side-config has priority). cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Enabled = false cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.enabled":true}}`), reply) } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, }}) } func TestTraceDisabledRemotelyOverridesServerSideConfig(t *testing.T) { // Test that the connect reply "collect_traces" setting overrides the // "transaction_tracer.enabled" server side config setting. cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.TransactionTracer.Enabled = true cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.enabled":true},"collect_traces":false}`), reply) } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceDisabledRemotely(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.DistributedTracer.Enabled = false } replyfn := func(reply *internal.ConnectReply) { reply.CollectTraces = false } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestTraceWithSegments(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 0 cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := txn.StartSegment("s1") s1.End() s2 := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com", } s2.End() s3 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "SELECT", } s3.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 3, }}) } func TestTraceSegmentsBelowThreshold(t *testing.T) { cfgfn := func(cfg *Config) { cfg.TransactionTracer.Threshold.IsApdexFailing = false cfg.TransactionTracer.Threshold.Duration = 0 cfg.TransactionTracer.Segments.Threshold = 1 * time.Hour cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") txn.SetWebRequestHTTP(helloRequest) s1 := txn.StartSegment("s1") s1.End() s2 := ExternalSegment{ StartTime: txn.StartSegmentNow(), URL: "http://example.com", } s2.End() s3 := DatastoreSegment{ StartTime: txn.StartSegmentNow(), Product: DatastoreMySQL, Collection: "my_table", Operation: "SELECT", } s3.End() txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{{ MetricName: "WebTransaction/Go/hello", NumSegments: 0, }}) } func TestNoticeErrorTxnEvents(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") txn.NoticeError(myError{}) app.expectNoLoggedErrors(t) txn.End() app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "error": true, }, }}) } func TestTransactionApplication(t *testing.T) { ap := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := ap.StartTransaction("hello") app := txn.Application() app.RecordCustomMetric("myMetric", 123.0) ap.expectNoLoggedErrors(t) expectData := []float64{1, 123.0, 123.0, 123.0, 123.0, 123.0 * 123.0} app.Private.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ {Name: "Custom/myMetric", Scope: "", Forced: false, Data: expectData}, }) } func TestNilSegmentPointerEnd(t *testing.T) { var basicSegment *Segment var datastoreSegment *DatastoreSegment var externalSegment *ExternalSegment // These calls on nil pointer receivers should not panic. basicSegment.End() datastoreSegment.End() externalSegment.End() } type flushWriter struct{} func (f flushWriter) WriteHeader(int) {} func (f flushWriter) Write([]byte) (int, error) { return 0, nil } func (f flushWriter) Header() http.Header { return nil } func (f flushWriter) Flush() {} func TestAsync(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s1 := txn.StartSegment("mainThread") asyncThread := txn.NewGoroutine() s2 := asyncThread.StartSegment("asyncThread") // End segments in interleaved order. s1.End() s2.End() // Test that the async transaction reference has the expected // transaction method behavior. asyncThread.AddAttribute("zip", "zap") // Test that the transaction ends when the async transaction is ended. asyncThread.End() app.expectNoLoggedErrors(t) threadAfterEnd := asyncThread.NewGoroutine() threadAfterEnd.End() app.expectSingleLoggedError(t, "unable to end transaction", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectTxnEvents(t, []internal.WantEvent{{ Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", }, UserAttributes: map[string]interface{}{ "zip": "zap", }, }}) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "Custom/mainThread", Scope: "", Forced: false, Data: nil}, {Name: "Custom/mainThread", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, {Name: "Custom/asyncThread", Scope: "", Forced: false, Data: nil}, {Name: "Custom/asyncThread", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, }) } func TestMessageProducerSegmentBasic(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() } cfgfn := func(cfg *Config) { cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: MessageQueue, DestinationName: "myQueue", } s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, }) app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "parentId": internal.MatchAnything, "name": "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", "category": "generic", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "sampled": true, "category": "generic", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestMessageProducerSegmentMissingDestinationType(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationName: "myQueue", } s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, }) } func TestMessageProducerSegmentTemp(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: MessageQueue, DestinationTemporary: true, DestinationName: "myQueue0123456789", } s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Temp", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Temp", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, }) } func TestMessageProducerSegmentNoName(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: MessageQueue, } s.End() app.expectNoLoggedErrors(t) txn.End() app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/Unknown", Scope: "", Forced: false, Data: nil}, {Name: "MessageBroker/RabbitMQ/Queue/Produce/Named/Unknown", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, }) } func TestMessageProducerSegmentTxnEnded(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) txn := app.StartTransaction("hello") s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: MessageQueue, DestinationTemporary: true, DestinationName: "myQueue0123456789", } txn.End() s.End() app.expectSingleLoggedError(t, "unable to end message producer segment", map[string]interface{}{ "reason": errAlreadyEnded.Error(), }) app.ExpectMetrics(t, []internal.WantMetric{ {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, }) } func TestMessageProducerSegmentNilTxn(t *testing.T) { var txn *Transaction s := MessageProducerSegment{ StartTime: txn.StartSegmentNow(), Library: "RabbitMQ", DestinationType: MessageQueue, DestinationTemporary: true, DestinationName: "myQueue0123456789", } s.End() } func TestMessageProducerSegmentNilSegment(t *testing.T) { var s *MessageProducerSegment s.End() } go-agent-3.42.0/v3/newrelic/internal_txn.go000066400000000000000000001101321510742411500204730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "fmt" "log" "net/http" "net/url" "reflect" "runtime/debug" "sync" "time" "github.com/newrelic/go-agent/v3/internal" ) type txn struct { app *app *appRun // This mutex is required since the consumer may call the public API // interface functions from different routines. sync.Mutex // finished indicates whether or not End() has been called. After // finished has been set to true, no recording should occur. finished bool numPayloadsCreated uint32 sampledCalculated bool ignore bool // wroteHeader prevents capturing multiple response code errors if the // user erroneously calls WriteHeader multiple times. wroteHeader bool txnData mainThread tracingThread asyncThreads []*tracingThread // csecData is used to propagate HTTP request context in async apps, // when NewGoroutine is called. csecData any csecAttributes map[string]any } type thread struct { *txn // thread does not have locking because it should only be accessed while // the txn is locked. thread *tracingThread } func (thd *thread) IsEnded() bool { txn := thd.txn txn.Lock() defer txn.Unlock() return txn.finished } func (txn *txn) markStart(now time.Time) { txn.Start = now // The mainThread is considered active now. txn.mainThread.RecordActivity(now) } func (txn *txn) markEnd(now time.Time, thread *tracingThread) { txn.Stop = now // The thread on which End() was called is considered active now. thread.RecordActivity(now) txn.Duration = txn.Stop.Sub(txn.Start) // TotalTime is the sum of "active time" across all threads. A thread // was active when it started the transaction, stopped the transaction, // started a segment, or stopped a segment. txn.TotalTime = txn.mainThread.TotalTime() for _, thd := range txn.asyncThreads { txn.TotalTime += thd.TotalTime() } // Ensure that TotalTime is at least as large as Duration so that the // graphs look sensible. This can happen under the following situation: // goroutine1: txn.start----|segment1| // goroutine2: |segment2|----txn.end if txn.Duration > txn.TotalTime { txn.TotalTime = txn.Duration } } func (txn *txn) setOption(opts ...TraceOption) { txnOpts := traceOptSet{} for _, o := range opts { o(&txnOpts) } // If we are suppressing code-level metrics but had already set up to report them, // remove those attributes now entirely. We've already spent the time to collect // the data, but that's water under the bridge at this point and the user is saying // explicitly they don't want them. if txnOpts.SuppressCLM { removeCodeLevelMetrics(txn.Attrs.Agent.Remove) } else if txn.appRun != nil && txn.appRun.Config.CodeLevelMetrics.Enabled && (txn.appRun.Config.CodeLevelMetrics.Scope == 0 || (txn.appRun.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { // If we're given an explicit code location to report, do that now. This will override // any previous code-level metrics information in the transaction. reportCodeLevelMetrics(txnOpts, txn.appRun, txn.Attrs.Agent.Add) } } func newTxn(app *app, run *appRun, name string, opts ...TraceOption) *thread { txn := &txn{ app: app, appRun: run, } txnOpts := traceOptSet{} for _, o := range opts { o(&txnOpts) } txn.markStart(time.Now()) txn.Name = name txn.Attrs = newAttributes(run.AttributeConfig) if !txnOpts.SuppressCLM && run.Config.CodeLevelMetrics.Enabled && (txnOpts.DemandCLM || run.Config.CodeLevelMetrics.Scope == 0 || (run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { reportCodeLevelMetrics(txnOpts, run, txn.Attrs.Agent.Add) } txn.TraceIDGenerator = run.Reply.TraceIDGenerator traceID := txn.TraceIDGenerator.GenerateTraceID() txn.SetTransactionID(traceID) if run.Config.DistributedTracer.Enabled { txn.BetterCAT.Enabled = true txn.BetterCAT.SetTraceAndTxnIDs(traceID) txn.BetterCAT.Priority = newPriorityFromRandom(txn.TraceIDGenerator.Float32) txn.ShouldCollectSpanEvents = txn.shouldCollectSpanEvents txn.ShouldCreateSpanGUID = txn.shouldCreateSpanGUID } txn.Attrs.Agent.Add(AttributeHostDisplayName, txn.Config.HostDisplayName, nil) txn.TxnTrace.Enabled = txn.Config.TransactionTracer.Enabled txn.TxnTrace.SegmentThreshold = txn.Config.TransactionTracer.Segments.Threshold txn.TxnTrace.StackTraceThreshold = txn.Config.TransactionTracer.Segments.StackTraceThreshold txn.SlowQueriesEnabled = txn.Config.DatastoreTracer.SlowQuery.Enabled txn.SlowQueryThreshold = txn.Config.DatastoreTracer.SlowQuery.Threshold // Synthetics support is tied up with a transaction's Old CAT field, // CrossProcess. To support Synthetics with either BetterCAT or Old CAT, // Initialize the CrossProcess field of the transaction, passing in // the top-level configuration. doOldCAT := txn.Config.CrossApplicationTracer.Enabled noGUID := txn.Config.DistributedTracer.Enabled txn.CrossProcess.Init(doOldCAT, noGUID, run.Reply) return &thread{ txn: txn, thread: &txn.mainThread, } } func (thd *thread) logAPIError(err error, operation string, extraDetails map[string]interface{}) { if nil == thd { return } if nil == err { return } if extraDetails == nil { extraDetails = make(map[string]interface{}, 1) } extraDetails["reason"] = err.Error() thd.Config.Logger.Error("unable to "+operation, extraDetails) } func (txn *txn) shouldCollectSpanEvents() bool { if !txn.Config.DistributedTracer.Enabled { return false } if !txn.Config.SpanEvents.Enabled { return false } if shouldUseTraceObserver(txn.Config) { return true } return txn.lazilyCalculateSampled() } func (txn *txn) shouldCreateSpanGUID() bool { if !txn.Config.DistributedTracer.Enabled { return false } if !txn.Config.SpanEvents.Enabled { return false } return true } // lazilyCalculateSampled calculates and returns whether or not the transaction // should be sampled. Sampled is not computed at the beginning of the // transaction because we want to calculate Sampled only for transactions that // do not accept an inbound payload. func (txn *txn) lazilyCalculateSampled() bool { if !txn.BetterCAT.Enabled { return false } if txn.sampledCalculated { return txn.BetterCAT.Sampled } txn.BetterCAT.Sampled = txn.appRun.adaptiveSampler.computeSampled(txn.BetterCAT.Priority.Float32(), time.Now()) if txn.BetterCAT.Sampled { txn.BetterCAT.Priority += 1.0 } txn.sampledCalculated = true return txn.BetterCAT.Sampled } func (txn *txn) SetWebRequest(r WebRequest) error { txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } // Any call to SetWebRequest should indicate a web transaction. txn.IsWeb = true h := r.Header if nil != h { txn.Queuing = queueDuration(h, txn.Start) txn.acceptDistributedTraceHeadersLocked(r.Transport, h) txn.CrossProcess.InboundHTTPRequest(h) } requestAgentAttributes(txn.Attrs, r.Method, h, r.URL, r.Host) return nil } type dummyResponseWriter struct{} func (rw dummyResponseWriter) Header() http.Header { return nil } func (rw dummyResponseWriter) Write(b []byte) (int, error) { return 0, nil } func (rw dummyResponseWriter) WriteHeader(code int) {} func (thd *thread) SetWebResponse(w http.ResponseWriter) http.ResponseWriter { txn := thd.txn txn.Lock() defer txn.Unlock() if w == nil { // Accepting a nil parameter makes it easy for consumers to add // a response code to the transaction without a response // writer: // // txn.SetWebResponse(nil).WriteHeader(500) // w = dummyResponseWriter{} } return upgradeResponseWriter(&replacementResponseWriter{ thd: thd, original: w, }) } func (thd *thread) StoreLog(log *logEvent) { txn := thd.txn txn.Lock() defer txn.Unlock() // might want to refactor to return errAlreadyEnded if txn.finished { return } if txn.logs == nil { txn.logs = make(logEventHeap, 0, internal.MaxLogEvents) } txn.logs.Add(log) } func (txn *txn) freezeName() { if txn.ignore || (txn.FinalName != "") { return } txn.FinalName = txn.appRun.createTransactionName(txn.Name, txn.IsWeb) if txn.FinalName == "" { txn.ignore = true } } func (txn *txn) getsApdex() bool { return txn.IsWeb && !txn.ignoreApdex } func (txn *txn) shouldSaveTrace() bool { if !txn.Config.TransactionTracer.Enabled { return false } if txn.CrossProcess.IsSynthetics() { return true } return txn.Duration >= txn.txnTraceThreshold(txn.ApdexThreshold) } func (txn *txn) MergeIntoHarvest(h *harvest) { var priority priority if txn.BetterCAT.Enabled { priority = txn.BetterCAT.Priority } else { priority = newPriority() } createTxnMetrics(&txn.txnData, h.Metrics) mergeBreakdownMetrics(&txn.txnData, h.Metrics) // Dump log events into harvest // Note: this will create a surge of log events that could affect sampling. for _, logEvent := range txn.logs { logEvent.priority = priority h.LogEvents.Add(&logEvent) } if txn.Config.TransactionEvents.Enabled { // Allocate a new TxnEvent to prevent a reference to the large transaction. alloc := new(txnEvent) *alloc = txn.txnData.txnEvent h.TxnEvents.AddTxnEvent(alloc, priority) } hs := &highSecuritySettings{txn.Config.HighSecurity, txn.Reply.SecurityPolicies.AllowRawExceptionMessages.Enabled()} if (txn.Reply.CollectErrors || txn.Config.ErrorCollector.CaptureEvents) && txn.Config.ErrorCollector.ErrorGroupCallback != nil { txn.txnEvent.errGroupCallback = txn.Config.ErrorCollector.ErrorGroupCallback for _, e := range txn.Errors { e.applyErrorGroup(&txn.txnEvent) } } if txn.Reply.CollectErrors { mergeTxnErrors(&h.ErrorTraces, txn.Errors, txn.txnEvent, hs) } if txn.Config.ErrorCollector.CaptureEvents { for _, e := range txn.Errors { e.scrubErrorForHighSecurity(hs) errEvent := &errorEvent{ errorData: *e, txnEvent: txn.txnEvent, } // Since the stack trace and raw error object is not used in error events, remove the reference // to minimize memory. errEvent.Stack = nil errEvent.RawError = nil h.ErrorEvents.Add(errEvent, priority) } } if txn.shouldSaveTrace() { h.TxnTraces.Witness(harvestTrace{ txnEvent: txn.txnEvent, Trace: txn.TxnTrace, }) } if nil != txn.SlowQueries { h.SlowSQLs.Merge(txn.SlowQueries, txn.txnEvent) } if txn.shouldCollectSpanEvents() && !shouldUseTraceObserver(txn.Config) { h.SpanEvents.MergeSpanEvents(txn.txnData.SpanEvents) } } func headersJustWritten(thd *thread, code int, hdr http.Header) { txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return } if txn.wroteHeader { return } txn.wroteHeader = true responseHeaderAttributes(txn.Attrs, hdr) responseCodeAttribute(txn.Attrs, code) if txn.appRun.responseCodeIsError(code) { e := txnErrorFromResponseCode(time.Now(), code) e.Stack = getStackTrace() expect := txn.appRun.responseCodeIsExpected(code) e.Expect = expect thd.noticeErrorInternal(e, nil, expect) } } func (txn *txn) responseHeader(hdr http.Header) http.Header { txn.Lock() defer txn.Unlock() if txn.finished { return nil } if txn.wroteHeader { return nil } if !txn.CrossProcess.Enabled { return nil } if !txn.CrossProcess.IsInbound() { return nil } txn.freezeName() contentLength := getContentLengthFromHeader(hdr) appData, err := txn.CrossProcess.CreateAppData(txn.FinalName, txn.Queuing, time.Since(txn.Start), contentLength) if err != nil { txn.Config.Logger.Debug("error generating outbound response header", map[string]interface{}{ "error": err, }) return nil } return appDataToHTTPHeader(appData) } func addCrossProcessHeaders(txn *txn, hdr http.Header) { // responseHeader() checks the wroteHeader field and returns a nil map if the // header has been written, so we don't need a check here. if nil != hdr { for key, values := range txn.responseHeader(hdr) { for _, value := range values { hdr.Add(key, value) } } } } func (thd *thread) End(recovered interface{}) error { txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } txn.finished = true if recovered != nil { e := txnErrorFromPanic(time.Now(), recovered) e.Stack = getStackTrace() thd.noticeErrorInternal(e, nil, false) log.Println(string(debug.Stack())) } txn.markEnd(time.Now(), thd.thread) txn.freezeName() // Make a sampling decision if there have been no segments or outbound // payloads. txn.lazilyCalculateSampled() // Finalise the CAT state. if err := txn.CrossProcess.Finalise(txn.Name, txn.Config.AppName); err != nil { txn.Config.Logger.Debug("error finalising the cross process state", map[string]interface{}{ "error": err, }) } // Assign apdexThreshold regardless of whether or not the transaction // gets apdex since it may be used to calculate the trace threshold. txn.ApdexThreshold = internal.CalculateApdexThreshold(txn.Reply, txn.FinalName) if txn.getsApdex() { if txn.HasErrors() && txn.NoticeErrors() { txn.Zone = apdexFailing } else { txn.Zone = calculateApdexZone(txn.ApdexThreshold, txn.Duration) } } else { txn.Zone = apdexNone } if txn.Config.Logger.DebugEnabled() { txn.Config.Logger.Debug("transaction ended", map[string]interface{}{ "name": txn.FinalName, "duration_ms": txn.Duration.Seconds() * 1000.0, "ignored": txn.ignore, "app_connected": txn.Reply.RunID != "", }) } if txn.shouldCollectSpanEvents() { root := &spanEvent{ GUID: txn.GetRootSpanID(), Timestamp: txn.Start, Duration: txn.Duration, Name: txn.FinalName, TxnName: txn.FinalName, Category: spanCategoryGeneric, IsEntrypoint: true, } root.AgentAttributes.addAgentAttrs(txn.Attrs.Agent) root.UserAttributes.addUserAttrs(txn.Attrs.user) if txn.rootSpanErrData != nil { root.AgentAttributes.addString(SpanAttributeErrorClass, txn.rootSpanErrData.Klass) root.AgentAttributes.addString(SpanAttributeErrorMessage, scrubbedErrorMessage(txn.rootSpanErrData.Msg, txn)) } if p := txn.BetterCAT.Inbound; nil != p { root.ParentID = txn.BetterCAT.Inbound.ID root.TrustedParentID = txn.BetterCAT.Inbound.TrustedParentID root.TracingVendors = txn.BetterCAT.Inbound.TracingVendors if p.HasNewRelicTraceInfo { root.AgentAttributes.addString("parent.type", p.Type) root.AgentAttributes.addString("parent.app", p.App) root.AgentAttributes.addString("parent.account", p.Account) root.AgentAttributes.addFloat("parent.transportDuration", p.TransportDuration.Seconds()) } root.AgentAttributes.addString("parent.transportType", txn.BetterCAT.TransportType) } root.AgentAttributes = txn.Attrs.filterSpanAttributes(root.AgentAttributes, destSpan) txn.SpanEvents = append(txn.SpanEvents, root) // Add transaction tracing fields to span events at the end of // the transaction since we could accept payload after the early // segments occur. for _, evt := range txn.SpanEvents { evt.TraceID = txn.BetterCAT.TraceID evt.TransactionID = txn.TxnID evt.Sampled = txn.BetterCAT.Sampled evt.Priority = txn.BetterCAT.Priority } } if !txn.ignore { txn.app.Consume(txn.Reply.RunID, txn) if observer := txn.app.getObserver(); nil != observer { for _, evt := range txn.SpanEvents { observer.consumeSpan(evt) } } } if recovered != nil { panic(recovered) } return nil } func (txn *txn) AddUserID(userID string) error { txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } txn.Attrs.Agent.Add(AttributeUserID, userID, nil) return nil } func (txn *txn) AddAttribute(name string, value interface{}) error { txn.Lock() defer txn.Unlock() if txn.Config.HighSecurity { return errHighSecurityEnabled } if !txn.Reply.SecurityPolicies.CustomParameters.Enabled() { return errSecurityPolicy } if txn.finished { return errAlreadyEnded } return addUserAttribute(txn.Attrs, name, value, destAll) } var ( errorsDisabled = errors.New("errors disabled") errNilError = errors.New("nil error") errAlreadyEnded = errors.New("transaction has already ended") errSecurityPolicy = errors.New("disabled by security policy") errTransactionIgnored = errors.New("transaction has been ignored") errBrowserDisabled = errors.New("browser disabled by local configuration") ) const ( highSecurityErrorMsg = "message removed by high security setting" securityPolicyErrorMsg = "message removed by security policy" ) func (thd *thread) noticeErrorInternal(errData errorData, err error, expect bool) error { txn := thd.txn if !txn.Config.ErrorCollector.Enabled { return errorsDisabled } if !expect { thd.noticeErrors = true } else { thd.expectedErrors = true } if nil == txn.Errors { txn.Errors = newTxnErrors(maxTxnErrors) } errData.RawError = err if txn.shouldCollectSpanEvents() { errData.SpanID = txn.CurrentSpanIdentifier(thd.thread) addErrorAttrs(thd, errData) } txn.Errors.Add(errData) txn.txnData.txnEvent.HasError = true //mark transaction as having an error return nil } var errorAttrs = []string{ SpanAttributeErrorClass, SpanAttributeErrorMessage, } func addErrorAttrs(t *thread, err errorData) { // If there are no current segments, we'll add them to the root span when it is created later if len(t.thread.stack) <= 0 { t.rootSpanErrData = &err return } for _, attr := range errorAttrs { t.thread.RemoveErrorSpanAttribute(attr) } t.thread.AddAgentSpanAttribute(SpanAttributeErrorClass, err.Klass) t.thread.AddAgentSpanAttribute(SpanAttributeErrorMessage, scrubbedErrorMessage(err.Msg, t.txn)) } var ( errTooManyErrorAttributes = fmt.Errorf("too many extra attributes: limit is %d", attributeErrorLimit) ) // errorCause returns the error's deepest wrapped ancestor. func errorCause(err error) error { for { if unwrapper, ok := err.(interface{ Unwrap() error }); ok { if next := unwrapper.Unwrap(); nil != next { err = next continue } } return err } } func errorClassMethod(err error) string { if ec, ok := err.(errorClasser); ok { return ec.ErrorClass() } return "" } func errorStackTraceMethod(err error) stackTrace { if st, ok := err.(stackTracer); ok { return st.StackTrace() } return nil } func errorAttributesMethod(err error) map[string]interface{} { if st, ok := err.(errorAttributer); ok { return st.ErrorAttributes() } return nil } func errDataFromError(input error, expect bool) (data errorData, err error) { cause := errorCause(input) validatedErrorMsg := truncateStringMessageIfLong(input.Error()) data = errorData{ When: time.Now(), Msg: validatedErrorMsg, Expect: expect, } if c := errorClassMethod(input); c != "" { // If the error implements ErrorClasser, use that. data.Klass = c } else if c := errorClassMethod(cause); c != "" { // Otherwise, if the error's cause implements ErrorClasser, use that. data.Klass = c } else { // As a final fallback, use the type of the error's cause. data.Klass = reflect.TypeOf(cause).String() } if st := errorStackTraceMethod(input); nil != st { // If the error implements StackTracer, use that. data.Stack = st } else if st := errorStackTraceMethod(cause); nil != st { // Otherwise, if the error's cause implements StackTracer, use that. data.Stack = st } else { // As a final fallback, generate a StackTrace here. data.Stack = getStackTrace() } var unvetted map[string]interface{} if ats := errorAttributesMethod(input); nil != ats { // If the error implements ErrorAttributer, use that. unvetted = ats } else { // Otherwise, if the error's cause implements ErrorAttributer, use that. unvetted = errorAttributesMethod(cause) } if unvetted != nil { if len(unvetted) > attributeErrorLimit { err = errTooManyErrorAttributes return } data.ExtraAttributes = make(map[string]interface{}) for key, val := range unvetted { val, err = validateUserAttribute(key, val) if nil != err { return } data.ExtraAttributes[key] = val } } return data, nil } func (thd *thread) NoticeError(input error, expect bool) error { txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } if nil == input { return errNilError } data, err := errDataFromError(input, expect) if nil != err { return err } if txn.Config.HighSecurity || !txn.Reply.SecurityPolicies.CustomParameters.Enabled() { data.ExtraAttributes = nil } return thd.noticeErrorInternal(data, input, expect) } func (txn *txn) SetName(name string) error { txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } txn.Name = name return nil } func (txn *txn) GetName() string { txn.Lock() defer txn.Unlock() return txn.Name } func (txn *txn) Ignore() error { txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } txn.ignore = true return nil } func (txn *txn) IgnoreApdex() error { txn.Lock() defer txn.Unlock() txn.ignoreApdex = true return nil } func (thd *thread) startSegmentAt(at time.Time) SegmentStartTime { var s segmentStartTime txn := thd.txn txn.Lock() if !txn.finished { s = startSegment(&txn.txnData, thd.thread, at) } txn.Unlock() return SegmentStartTime{ start: s, thread: thd, } } const ( // Browser fields are encoded using the first digits of the license // key. browserEncodingKeyLimit = 13 ) func browserEncodingKey(licenseKey string) []byte { key := []byte(licenseKey) if len(key) > browserEncodingKeyLimit { key = key[0:browserEncodingKeyLimit] } return key } func (txn *txn) BrowserTimingHeader() (*BrowserTimingHeader, error) { txn.Lock() defer txn.Unlock() if !txn.Config.BrowserMonitoring.Enabled { return nil, errBrowserDisabled } if txn.Reply.AgentLoader == "" { // If the loader is empty, either browser has been disabled // by the server or the application is not yet connected. return nil, nil } if txn.finished { return nil, errAlreadyEnded } txn.freezeName() // Freezing the name might cause the transaction to be ignored, so check // this after txn.freezeName(). if txn.ignore { return nil, errTransactionIgnored } encodingKey := browserEncodingKey(txn.Config.License) attrs, err := obfuscate(browserAttributes(txn.Attrs), encodingKey) if err != nil { return nil, fmt.Errorf("error getting browser attributes: %v", err) } name, err := obfuscate([]byte(txn.FinalName), encodingKey) if err != nil { return nil, fmt.Errorf("error obfuscating name: %v", err) } return &BrowserTimingHeader{ agentLoader: txn.Reply.AgentLoader, info: browserInfo{ Beacon: txn.Reply.Beacon, LicenseKey: txn.Reply.BrowserKey, ApplicationID: txn.Reply.AppID, TransactionName: name, QueueTimeMillis: txn.Queuing.Nanoseconds() / (1000 * 1000), ApplicationTimeMillis: time.Since(txn.Start).Nanoseconds() / (1000 * 1000), ObfuscatedAttributes: attrs, ErrorBeacon: txn.Reply.ErrorBeacon, Agent: txn.Reply.JSAgentFile, }, }, nil } func createThread(txn *txn) *tracingThread { newThread := newTracingThread(&txn.txnData) txn.asyncThreads = append(txn.asyncThreads, newThread) return newThread } func (thd *thread) NewGoroutine() *Transaction { txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { // If the transaction has finished, return the same thread. return newTransaction(thd) } return newTransaction(&thread{ thread: createThread(txn), txn: txn, }) } func endBasic(s *Segment) error { thd := s.StartTime.thread if nil == thd { return nil } txn := thd.txn var err error txn.Lock() if txn.finished { err = errAlreadyEnded } else { err = endBasicSegment(&txn.txnData, thd.thread, s.StartTime.start, time.Now(), s.Name) } txn.Unlock() return err } func endDatastore(s *DatastoreSegment) error { thd := s.StartTime.thread if nil == thd { return nil } txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } if txn.Config.HighSecurity { s.QueryParameters = nil } if !txn.Config.DatastoreTracer.QueryParameters.Enabled { s.QueryParameters = nil } if txn.Config.DatastoreTracer.RawQuery.Enabled { s.ParameterizedQuery = s.RawQuery } if txn.Reply.SecurityPolicies.RecordSQL.IsSet() { s.QueryParameters = nil if !txn.Reply.SecurityPolicies.RecordSQL.Enabled() { s.ParameterizedQuery = "" } } if !txn.Config.DatastoreTracer.DatabaseNameReporting.Enabled { s.DatabaseName = "" } if !txn.Config.DatastoreTracer.InstanceReporting.Enabled { s.Host = "" s.PortPathOrID = "" } return endDatastoreSegment(endDatastoreParams{ TxnData: &txn.txnData, Thread: thd.thread, Start: s.StartTime.start, Now: time.Now(), Product: string(s.Product), Collection: s.Collection, Operation: s.Operation, ParameterizedQuery: s.ParameterizedQuery, QueryParameters: s.QueryParameters, Host: s.Host, PortPathOrID: s.PortPathOrID, Database: s.DatabaseName, ThisHost: txn.appRun.Config.hostname, }) } func externalSegmentMethod(s *ExternalSegment) string { if s.Procedure != "" { return s.Procedure } r := s.Request if nil != s.Response && nil != s.Response.Request { r = s.Response.Request } if nil != r { if r.Method != "" { return r.Method } // Golang's http package states that when a client's Request has // an empty string for Method, the method is GET. return "GET" } return "" } func externalSegmentURL(s *ExternalSegment) (*url.URL, error) { if "" != s.URL { return url.Parse(s.URL) } r := s.Request if nil != s.Response && nil != s.Response.Request { r = s.Response.Request } if r != nil { return r.URL, nil } return nil, nil } func endExternal(s *ExternalSegment) error { thd := s.StartTime.thread if nil == thd { return nil } txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } u, err := externalSegmentURL(s) if nil != err { return err } return endExternalSegment(endExternalParams{ TxnData: &txn.txnData, Thread: thd.thread, Start: s.StartTime.start, Now: time.Now(), Logger: txn.Config.Logger, Response: s.Response, URL: u, Host: s.Host, Library: s.Library, Method: externalSegmentMethod(s), StatusCode: s.statusCode, }) } func endMessage(s *MessageProducerSegment) error { thd := s.StartTime.thread if nil == thd { return nil } txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return errAlreadyEnded } if s.DestinationType == "" { s.DestinationType = MessageQueue } return endMessageSegment(endMessageParams{ TxnData: &txn.txnData, Thread: thd.thread, Start: s.StartTime.start, Now: time.Now(), Library: s.Library, Logger: txn.Config.Logger, DestinationName: s.DestinationName, DestinationType: string(s.DestinationType), DestinationTemp: s.DestinationTemporary, }) } // oldCATOutboundHeaders generates the Old CAT and Synthetics headers, depending // on whether Old CAT is enabled or any Synthetics functionality has been // triggered in the agent. func oldCATOutboundHeaders(txn *txn) http.Header { txn.Lock() defer txn.Unlock() if txn.finished { return http.Header{} } metadata, err := txn.CrossProcess.CreateCrossProcessMetadata(txn.Name, txn.Config.AppName) if err != nil { txn.Config.Logger.Debug("error generating outbound headers", map[string]interface{}{ "error": err, }) // It's possible for CreateCrossProcessMetadata() to error and still have a // Synthetics header, so we'll still fall through to returning headers // based on whatever metadata was returned. } return metadataToHTTPHeader(metadata) } func outboundHeaders(s *ExternalSegment) http.Header { thd := s.StartTime.thread if nil == thd { return http.Header{} } txn := thd.txn hdr := oldCATOutboundHeaders(txn) // hdr may be empty, or it may contain headers. If DistributedTracer // is enabled, add more to the existing hdr thd.CreateDistributedTracePayload(hdr) return hdr } const ( maxSampledDistributedPayloads = 35 ) func (thd *thread) CreateDistributedTracePayload(hdrs http.Header) { txn := thd.txn txn.Lock() defer txn.Unlock() if !txn.BetterCAT.Enabled { return } support := &txn.DistributedTracingSupport excludeNRHeader := thd.Config.DistributedTracer.ExcludeNewRelicHeader if txn.finished { support.TraceContextCreateException = true if !excludeNRHeader { support.CreatePayloadException = true } return } if txn.Reply.AccountID == "" || txn.Reply.TrustedAccountKey == "" { // We can't create a payload: The application is not yet // connected or serverless distributed tracing configuration was // not provided. return } txn.numPayloadsCreated++ p := &payload{} // Calculate sampled first since this also changes the value for the // priority sampled := txn.lazilyCalculateSampled() if txn.shouldCreateSpanGUID() { p.ID = txn.CurrentSpanIdentifier(thd.thread) } p.Type = callerTypeApp p.Account = txn.Reply.AccountID p.App = txn.Reply.PrimaryAppID p.TracedID = txn.BetterCAT.TraceID p.Timestamp.Set(txn.Reply.DistributedTraceTimestampGenerator()) p.TrustedAccountKey = txn.Reply.TrustedAccountKey p.TransactionID = txn.TxnID // Set the transaction ID to the transaction guid. if nil != txn.BetterCAT.Inbound { p.NonTrustedTraceState = txn.BetterCAT.Inbound.NonTrustedTraceState p.OriginalTraceState = txn.BetterCAT.Inbound.OriginalTraceState } // limit the number of outbound sampled=true payloads to prevent too // many downstream sampled events. p.SetSampled(false) if txn.numPayloadsCreated < maxSampledDistributedPayloads { p.SetSampled(sampled) } if p.isSampled() { // trace parent exists and is sampled switch txn.Config.DistributedTracer.Sampler.RemoteParentSampled { case "always_on": p.Priority = 2.0 txn.BetterCAT.Priority = p.Priority case "always_off": p.Priority = 0.0 txn.BetterCAT.Priority = p.Priority default: // If the remote parent is sampled, we use the priority from the // transaction's adaptive sampler. p.Priority = txn.BetterCAT.Priority } } else { switch txn.Config.DistributedTracer.Sampler.RemoteParentNotSampled { case "always_on": p.Priority = 2.0 txn.BetterCAT.Priority = p.Priority case "always_off": p.Priority = 0.0 txn.BetterCAT.Priority = p.Priority default: // If the remote parent is not sampled, we use the priority from the // transaction's adaptive sampler. p.Priority = txn.BetterCAT.Priority } } support.TraceContextCreateSuccess = true if !excludeNRHeader { hdrs.Set(DistributedTraceNewRelicHeader, p.NRHTTPSafe()) support.CreatePayloadSuccess = true } // ID must be present in the Traceparent header when span events are // enabled, even if the transaction is not sampled. Note that this // assignment occurs after setting the Newrelic header since the ID // field of the Newrelic header should be empty if span events are // disabled or the transaction is not sampled. if p.ID == "" { p.ID = txn.CurrentSpanIdentifier(thd.thread) } hdrs.Set(DistributedTraceW3CTraceParentHeader, p.W3CTraceParent()) if !txn.Config.SpanEvents.Enabled { p.ID = "" } if !txn.Config.TransactionEvents.Enabled { p.TransactionID = "" } hdrs.Set(DistributedTraceW3CTraceStateHeader, p.W3CTraceState()) } var ( errOutboundPayloadCreated = errors.New("outbound payload already created") errAlreadyAccepted = errors.New("AcceptDistributedTraceHeaders has already been called") errInboundPayloadDTDisabled = errors.New("DistributedTracer must be enabled to accept an inbound payload") errTrustedAccountKey = errors.New("trusted account key missing or does not match") ) func (txn *txn) AcceptDistributedTraceHeaders(t TransportType, hdrs http.Header) error { txn.Lock() defer txn.Unlock() return txn.acceptDistributedTraceHeadersLocked(t, hdrs) } func (txn *txn) acceptDistributedTraceHeadersLocked(t TransportType, hdrs http.Header) error { if !txn.BetterCAT.Enabled { return errInboundPayloadDTDisabled } if txn.finished { return errAlreadyEnded } support := &txn.DistributedTracingSupport if txn.numPayloadsCreated > 0 { support.AcceptPayloadCreateBeforeAccept = true return errOutboundPayloadCreated } if txn.BetterCAT.Inbound != nil { support.AcceptPayloadIgnoredMultiple = true return errAlreadyAccepted } if hdrs == nil { support.AcceptPayloadNullPayload = true return nil } if txn.Reply.AccountID == "" || txn.Reply.TrustedAccountKey == "" { // We can't accept a payload: The application is not yet // connected or serverless distributed tracing configuration was // not provided. return nil } txn.BetterCAT.TransportType = t.toString() payload, err := acceptPayload(hdrs, txn.Reply.TrustedAccountKey, support) if err != nil { return err } if payload == nil { return nil } // and let's also do our trustedKey check receivedTrustKey := payload.TrustedAccountKey if receivedTrustKey == "" { receivedTrustKey = payload.Account } // If the trust key doesn't match but we don't have any New Relic trace info, this means // we just got the TraceParent header, and we still need to save that info to BetterCAT // farther down. if receivedTrustKey != txn.Reply.TrustedAccountKey && payload.HasNewRelicTraceInfo { support.AcceptPayloadUntrustedAccount = true return errTrustedAccountKey } if payload.Priority != 0 { txn.BetterCAT.Priority = payload.Priority } // a nul payload.Sampled means the a field wasn't provided if nil != payload.Sampled { txn.BetterCAT.Sampled = *payload.Sampled txn.sampledCalculated = true } txn.BetterCAT.Inbound = payload txn.BetterCAT.TraceID = payload.TracedID if tm := payload.Timestamp.Time(); txn.Start.After(tm) { txn.BetterCAT.Inbound.TransportDuration = txn.Start.Sub(tm) } return nil } func (txn *txn) Application() *Application { return newApplication(txn.app) } // Note that Agent attributes added to spans must be on the allowed list of // span attributes, which you can find in attributes.go func (thd *thread) AddAgentSpanAttribute(key string, val string) { txn := thd.txn txn.Lock() defer txn.Unlock() thd.thread.AddAgentSpanAttribute(key, val) } func (thd *thread) AddUserSpanAttribute(key string, val interface{}) error { txn := thd.txn txn.Lock() defer txn.Unlock() if outputDests := applyAttributeConfig(thd.Attrs.config, key, destSpan); outputDests == 0 { return nil } if txn.Config.HighSecurity { return errHighSecurityEnabled } if !txn.Reply.SecurityPolicies.CustomParameters.Enabled() { return errSecurityPolicy } thd.thread.AddUserSpanAttribute(key, val) return nil } var ( // Ensure that txn implements AddAgentAttributer to avoid breaking // integration package type assertions. _ internal.AddAgentAttributer = &txn{} ) func (txn *txn) AddAgentAttribute(name string, stringVal string, otherVal interface{}) { txn.Lock() defer txn.Unlock() if txn.finished { return } txn.Attrs.Agent.Add(name, stringVal, otherVal) } func (thd *thread) GetTraceMetadata() (metadata TraceMetadata) { txn := thd.txn txn.Lock() defer txn.Unlock() if txn.finished { return } if txn.BetterCAT.Enabled { metadata.TraceID = txn.BetterCAT.TraceID if txn.shouldCollectSpanEvents() { metadata.SpanID = txn.CurrentSpanIdentifier(thd.thread) } } return } func (thd *thread) GetLinkingMetadata() (metadata LinkingMetadata) { txn := thd.txn metadata.EntityName = txn.appRun.firstAppName metadata.EntityType = "SERVICE" metadata.EntityGUID = txn.appRun.Reply.EntityGUID metadata.Hostname = txn.appRun.Config.hostname md := thd.GetTraceMetadata() metadata.TraceID = md.TraceID metadata.SpanID = md.SpanID return } func (txn *txn) IsSampled() bool { txn.Lock() defer txn.Unlock() if txn.finished { return false } return txn.lazilyCalculateSampled() } func (txn *txn) getCsecData() any { txn.Lock() defer txn.Unlock() return txn.csecData } func (txn *txn) setCsecData() { txn.Lock() defer txn.Unlock() if txn.csecData == nil && IsSecurityAgentPresent() { txn.csecData = secureAgent.SendEvent("NEW_GOROUTINE", "") } } func (txn *txn) getCsecAttributes() map[string]any { txn.Lock() defer txn.Unlock() if txn.csecAttributes == nil { return map[string]any{} } return txn.csecAttributes } func (txn *txn) setCsecAttributes(key string, value any) { txn.Lock() defer txn.Unlock() if txn.csecAttributes == nil { txn.csecAttributes = map[string]any{} } txn.csecAttributes[key] = value } go-agent-3.42.0/v3/newrelic/internal_txn_test.go000066400000000000000000000741121510742411500215410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "net/http" "reflect" "runtime" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" ) func TestShouldSaveTrace(t *testing.T) { for _, tc := range []struct { name string expected bool synthetics bool tracerEnabled bool collectTraces bool duration time.Duration threshold time.Duration }{ { name: "insufficient duration, all disabled", expected: false, synthetics: false, tracerEnabled: false, collectTraces: false, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "insufficient duration, only synthetics enabled", expected: false, synthetics: true, tracerEnabled: false, collectTraces: false, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "insufficient duration, only tracer enabled", expected: false, synthetics: false, tracerEnabled: true, collectTraces: false, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "insufficient duration, only collect traces enabled", expected: false, synthetics: false, tracerEnabled: false, collectTraces: true, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "insufficient duration, all normal flags enabled", expected: false, synthetics: false, tracerEnabled: true, collectTraces: true, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "insufficient duration, all flags enabled", expected: true, synthetics: true, tracerEnabled: true, collectTraces: true, duration: 1 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, all disabled", expected: false, synthetics: false, tracerEnabled: false, collectTraces: false, duration: 3 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, only synthetics enabled", expected: false, synthetics: true, tracerEnabled: false, collectTraces: false, duration: 3 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, only tracer enabled", expected: false, synthetics: false, tracerEnabled: true, collectTraces: false, duration: 3 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, only collect traces enabled", expected: false, synthetics: false, tracerEnabled: false, collectTraces: true, duration: 3 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, all normal flags enabled", expected: true, synthetics: false, tracerEnabled: true, collectTraces: true, duration: 3 * time.Second, threshold: 2 * time.Second, }, { name: "sufficient duration, all flags enabled", expected: true, synthetics: true, tracerEnabled: true, collectTraces: true, duration: 3 * time.Second, threshold: 2 * time.Second, }, } { txn := &txn{} cfg := defaultConfig() cfg.TransactionTracer.Enabled = tc.tracerEnabled cfg.TransactionTracer.Threshold.Duration = tc.threshold cfg.TransactionTracer.Threshold.IsApdexFailing = false reply := internal.ConnectReplyDefaults() reply.CollectTraces = tc.collectTraces txn.appRun = newAppRun(config{Config: cfg}, reply) txn.Duration = tc.duration if tc.synthetics { txn.CrossProcess.Synthetics = &cat.SyntheticsHeader{} txn.CrossProcess.SetSynthetics(tc.synthetics) } if actual := txn.shouldSaveTrace(); actual != tc.expected { t.Errorf("%s: unexpected shouldSaveTrace value; expected %v; got %v", tc.name, tc.expected, actual) } } } func TestLazilyCalculateSampledTrue(t *testing.T) { tx := &txn{} tx.BetterCAT.Priority = 0.5 tx.sampledCalculated = false tx.BetterCAT.Enabled = true cfg := config{Config: defaultConfig()} reply := &internal.ConnectReply{} reply.SetSampleEverything() tx.appRun = newAppRun(cfg, reply) out := tx.lazilyCalculateSampled() if !out || !tx.BetterCAT.Sampled || !tx.sampledCalculated || tx.BetterCAT.Priority != 1.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } tx.Reply.SetSampleNothing() out = tx.lazilyCalculateSampled() if !out || !tx.BetterCAT.Sampled || !tx.sampledCalculated || tx.BetterCAT.Priority != 1.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } } func TestLazilyCalculateSampledFalse(t *testing.T) { tx := &txn{} tx.BetterCAT.Priority = 0.5 tx.sampledCalculated = false tx.BetterCAT.Enabled = true cfg := config{Config: defaultConfig()} reply := &internal.ConnectReply{} reply.SetSampleNothing() tx.appRun = newAppRun(cfg, reply) out := tx.lazilyCalculateSampled() if out || tx.BetterCAT.Sampled || !tx.sampledCalculated || tx.BetterCAT.Priority != 0.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } tx.Reply.SetSampleEverything() out = tx.lazilyCalculateSampled() if out || tx.BetterCAT.Sampled || !tx.sampledCalculated || tx.BetterCAT.Priority != 0.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } } func TestLazilyCalculateSampledCATDisabled(t *testing.T) { tx := &txn{} tx.appRun = &appRun{} tx.BetterCAT.Priority = 0.5 tx.sampledCalculated = false tx.BetterCAT.Enabled = false tx.Reply = &internal.ConnectReply{} tx.Reply.SetSampleEverything() out := tx.lazilyCalculateSampled() if out || tx.BetterCAT.Sampled || tx.sampledCalculated || tx.BetterCAT.Priority != 0.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } out = tx.lazilyCalculateSampled() if out || tx.BetterCAT.Sampled || tx.sampledCalculated || tx.BetterCAT.Priority != 0.5 { t.Error(out, tx.BetterCAT.Sampled, tx.sampledCalculated, tx.BetterCAT.Priority) } } type expectTxnTimes struct { txn *txn testName string start time.Time stop time.Time duration time.Duration totalTime time.Duration } func TestTransactionDurationTotalTime(t *testing.T) { // These tests touch internal txn structures rather than the public API: // Testing duration and total time is tough because our API functions do // not take fixed times. start := time.Now() testTxnTimes := func(expect expectTxnTimes) { if expect.txn.Start != expect.start { t.Error("start time", expect.testName, expect.txn.Start, expect.start) } if expect.txn.Stop != expect.stop { t.Error("stop time", expect.testName, expect.txn.Stop, expect.stop) } if expect.txn.Duration != expect.duration { t.Error("duration", expect.testName, expect.txn.Duration, expect.duration) } if expect.txn.TotalTime != expect.totalTime { t.Error("total time", expect.testName, expect.txn.TotalTime, expect.totalTime) } } // Basic transaction with no async activity. tx := &txn{} tx.markStart(start) segmentStart := startSegment(&tx.txnData, &tx.mainThread, start.Add(1*time.Second)) endBasicSegment(&tx.txnData, &tx.mainThread, segmentStart, start.Add(2*time.Second), "name") tx.markEnd(start.Add(3*time.Second), &tx.mainThread) testTxnTimes(expectTxnTimes{ txn: tx, testName: "basic transaction", start: start, stop: start.Add(3 * time.Second), duration: 3 * time.Second, totalTime: 3 * time.Second, }) // Transaction with async activity. tx = &txn{} tx.markStart(start) segmentStart = startSegment(&tx.txnData, &tx.mainThread, start.Add(1*time.Second)) endBasicSegment(&tx.txnData, &tx.mainThread, segmentStart, start.Add(2*time.Second), "name") asyncThread := createThread(tx) asyncSegmentStart := startSegment(&tx.txnData, asyncThread, start.Add(1*time.Second)) endBasicSegment(&tx.txnData, asyncThread, asyncSegmentStart, start.Add(2*time.Second), "name") tx.markEnd(start.Add(3*time.Second), &tx.mainThread) testTxnTimes(expectTxnTimes{ txn: tx, testName: "transaction with async activity", start: start, stop: start.Add(3 * time.Second), duration: 3 * time.Second, totalTime: 4 * time.Second, }) // Transaction ended on async thread. tx = &txn{} tx.markStart(start) segmentStart = startSegment(&tx.txnData, &tx.mainThread, start.Add(1*time.Second)) endBasicSegment(&tx.txnData, &tx.mainThread, segmentStart, start.Add(2*time.Second), "name") asyncThread = createThread(tx) asyncSegmentStart = startSegment(&tx.txnData, asyncThread, start.Add(1*time.Second)) endBasicSegment(&tx.txnData, asyncThread, asyncSegmentStart, start.Add(2*time.Second), "name") tx.markEnd(start.Add(3*time.Second), asyncThread) testTxnTimes(expectTxnTimes{ txn: tx, testName: "transaction ended on async thread", start: start, stop: start.Add(3 * time.Second), duration: 3 * time.Second, totalTime: 4 * time.Second, }) // Duration exceeds TotalTime. tx = &txn{} tx.markStart(start) segmentStart = startSegment(&tx.txnData, &tx.mainThread, start.Add(0*time.Second)) endBasicSegment(&tx.txnData, &tx.mainThread, segmentStart, start.Add(1*time.Second), "name") asyncThread = createThread(tx) asyncSegmentStart = startSegment(&tx.txnData, asyncThread, start.Add(2*time.Second)) endBasicSegment(&tx.txnData, asyncThread, asyncSegmentStart, start.Add(3*time.Second), "name") tx.markEnd(start.Add(3*time.Second), asyncThread) testTxnTimes(expectTxnTimes{ txn: tx, testName: "TotalTime should be at least Duration", start: start, stop: start.Add(3 * time.Second), duration: 3 * time.Second, totalTime: 3 * time.Second, }) } var ( replyFn = func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgFn = func(cfg *Config) { cfg.DistributedTracer.Enabled = true } ) func TestGetTraceMetadataDistributedTracingDisabled(t *testing.T) { cfgFnDTDisabled := func(cfg *Config) { cfg.DistributedTracer.Enabled = false } app := testApp(replyFn, cfgFnDTDisabled, t) txn := app.StartTransaction("hello") metadata := txn.GetTraceMetadata() if metadata.SpanID != "" { t.Error(metadata.SpanID) } if metadata.TraceID != "" { t.Error(metadata.TraceID) } } func TestGetTraceMetadataSuccess(t *testing.T) { app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") metadata := txn.GetTraceMetadata() if metadata.SpanID != "e71870997d57214c" { t.Error(metadata.SpanID) } if metadata.TraceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error(metadata.TraceID) } txn.StartSegment("name") // Span id should be different now that a segment has started. metadata = txn.GetTraceMetadata() if metadata.SpanID != "4259d74b863e2fba" { t.Error(metadata.SpanID) } if metadata.TraceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error(metadata.TraceID) } } func TestGetTraceMetadataEnded(t *testing.T) { // Test that GetTraceMetadata returns empty strings if the transaction // has been finished. app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") txn.End() metadata := txn.GetTraceMetadata() if metadata.SpanID != "" { t.Error(metadata.SpanID) } if metadata.TraceID != "" { t.Error(metadata.TraceID) } } func TestGetTraceMetadataNotSampled(t *testing.T) { replyFnNotSampled := func(reply *internal.ConnectReply) { reply.SetSampleNothing() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } app := testApp(replyFnNotSampled, cfgFn, t) txn := app.StartTransaction("hello") metadata := txn.GetTraceMetadata() if metadata.SpanID != "" { t.Error(metadata.SpanID) } if metadata.TraceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error(metadata.TraceID) } } func TestGetTraceMetadataSpanEventsDisabled(t *testing.T) { cfgFnSpansDisabled := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = false } app := testApp(replyFn, cfgFnSpansDisabled, t) txn := app.StartTransaction("hello") metadata := txn.GetTraceMetadata() if metadata.SpanID != "" { t.Error(metadata.SpanID) } if metadata.TraceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error(metadata.TraceID) } } func TestGetTraceMetadataInboundPayload(t *testing.T) { replyFnWithAccountInfo := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) reply.AccountID = "account-id" reply.TrustedAccountKey = "123" reply.PrimaryAppID = "app-id" } app := testApp(replyFnWithAccountInfo, cfgFn, t) hdrs := http.Header{} hdrs.Set(DistributedTraceW3CTraceParentHeader, "00-12345678901234567890123456789012-9566c74d10037c4d-01") hdrs.Set(DistributedTraceW3CTraceStateHeader, "123@nr=0-0-123-456-9566c74d10037c4d-52fdfc072182654f-1-0.390345-1563574856827") txn := app.StartTransaction("hello") txn.AcceptDistributedTraceHeaders(TransportHTTP, hdrs) app.expectNoLoggedErrors(t) metadata := txn.GetTraceMetadata() if metadata.SpanID != "e71870997d57214c" { t.Errorf("Invalid Span ID, expected aeceb05d2fdcde0c but got %s", metadata.SpanID) } if metadata.TraceID != "12345678901234567890123456789012" { t.Errorf("Invalid Trace ID, expected 12345678901234567890123456789012 but got %s", metadata.TraceID) } } func TestGetLinkingMetadata(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = "entities-are-guid" reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgfn := func(cfg *Config) { cfg.AppName = "app-name" cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") metadata := txn.GetLinkingMetadata() host := txn.thread.appRun.Config.hostname if metadata.TraceID != "1ae969564b34a33ecd1af05fe6923d6d" { t.Error("wrong TraceID:", metadata.TraceID) } if metadata.SpanID != "e71870997d57214c" { t.Error("wrong SpanID:", metadata.SpanID) } if metadata.EntityName != "app-name" { t.Error("wrong EntityName:", metadata.EntityName) } if metadata.EntityType != "SERVICE" { t.Error("wrong EntityType:", metadata.EntityType) } if metadata.EntityGUID != "entities-are-guid" { t.Error("wrong EntityGUID:", metadata.EntityGUID) } if metadata.Hostname != host { t.Error("wrong Hostname:", metadata.Hostname) } } func TestGetLinkingMetadataAppNames(t *testing.T) { testcases := []struct { appName string expected string }{ {appName: "one-name", expected: "one-name"}, {appName: "one-name;two-name;three-name", expected: "one-name"}, {appName: "", expected: ""}, } for _, test := range testcases { cfgfn := func(cfg *Config) { cfg.AppName = test.appName } app := testApp(nil, cfgfn, t) txn := app.StartTransaction("hello") metadata := txn.GetLinkingMetadata() if metadata.EntityName != test.expected { t.Errorf("wrong EntityName, actual=%s expected=%s", metadata.EntityName, test.expected) } } } func TestIsSampledFalse(t *testing.T) { replyFnSampleNothing := func(reply *internal.ConnectReply) { reply.SetSampleNothing() } app := testApp(replyFnSampleNothing, cfgFn, t) txn := app.StartTransaction("hello") sampled := txn.IsSampled() if sampled == true { t.Error("txn should not be sampled") } } func TestIsSampledTrue(t *testing.T) { app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") sampled := txn.IsSampled() if sampled == false { t.Error("txn should be sampled") } } func TestIsSampledEnded(t *testing.T) { // Test that Transaction.IsSampled returns false if the transaction has // already ended. app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") txn.End() sampled := txn.IsSampled() if sampled == true { t.Error("finished txn should not be sampled") } } func TestNilTransaction(t *testing.T) { var txn *Transaction txn.End() txn.Ignore() txn.SetName("hello") txn.NoticeError(errors.New("something")) txn.AddAttribute("myKey", "myValue") txn.SetWebRequestHTTP(helloRequest) var x dummyResponseWriter if w := txn.SetWebResponse(x); w != x { t.Error(w) } if start := txn.StartSegmentNow(); !reflect.DeepEqual(start, SegmentStartTime{}) { t.Error(start) } if seg := txn.StartSegment("hello"); !reflect.DeepEqual(seg, &Segment{Name: "hello"}) { t.Error(seg) } hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) > 0 { t.Error(hdrs) } txn.AcceptDistributedTraceHeaders(TransportHTTP, nil) if app := txn.Application(); app != nil { t.Error(app) } if hdr := txn.BrowserTimingHeader(); hdr.WithTags() != nil { t.Error(hdr) } if tx := txn.NewGoroutine(); tx != nil { t.Error(tx) } if m := txn.GetTraceMetadata(); !reflect.DeepEqual(m, TraceMetadata{}) { t.Error(m) } if m := txn.GetLinkingMetadata(); !reflect.DeepEqual(m, LinkingMetadata{}) { t.Error(m) } if s := txn.IsSampled(); s { t.Error(s) } } func TestGetName(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = "entities-are-guid" reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgfn := func(cfg *Config) { cfg.AppName = "app-name" cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") defer txn.End() txn.Ignore() txn.SetName("hello世界") if theName := txn.Name(); theName != "hello世界" { t.Error(theName) } } func TestIgnoreTransaction(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = "entities-are-guid" reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgfn := func(cfg *Config) { cfg.AppName = "app-name" cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.Ignore() txn.SetName("hello世界") txn.NoticeError(errors.New("hi")) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestIgnoreApdex(t *testing.T) { replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.EntityGUID = "entities-are-guid" reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) } cfgfn := func(cfg *Config) { cfg.AppName = "app-name" cfg.DistributedTracer.Enabled = true } app := testApp(replyfn, cfgfn, t) txn := app.StartTransaction("hello") txn.IgnoreApdex() txn.SetName("hello世界") txn.NoticeError(errors.New("hi")) txn.End() app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) } func TestEmptyTransaction(t *testing.T) { txn := &Transaction{} txn.End() txn.Ignore() txn.SetName("hello") txn.NoticeError(errors.New("something")) txn.AddAttribute("myKey", "myValue") txn.SetWebRequestHTTP(helloRequest) var x dummyResponseWriter if w := txn.SetWebResponse(x); w != x { t.Error(w) } if start := txn.StartSegmentNow(); !reflect.DeepEqual(start, SegmentStartTime{}) { t.Error(start) } hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) if len(hdrs) > 0 { t.Error(hdrs) } txn.AcceptDistributedTraceHeaders(TransportHTTP, nil) if app := txn.Application(); app != nil { t.Error(app) } if hdr := txn.BrowserTimingHeader(); hdr.WithTags() != nil { t.Error(hdr) } if tx := txn.NewGoroutine(); tx != nil { t.Error(tx) } if m := txn.GetTraceMetadata(); !reflect.DeepEqual(m, TraceMetadata{}) { t.Error(m) } if m := txn.GetLinkingMetadata(); !reflect.DeepEqual(m, LinkingMetadata{}) { t.Error(m) } if s := txn.IsSampled(); s { t.Error(s) } } func TestDTPriority(t *testing.T) { type testCase struct { name string incomingSampledAndPriority string expectedPriority string } // We expect to either receive both a priority and a sampled field, or neither - not one without the other. cases := []testCase{ { name: "IncludesIncomingPriority", incomingSampledAndPriority: `,"sa":true,"pr":1.5`, expectedPriority: "1.5", }, { name: "NoIncomingPriority", incomingSampledAndPriority: "", expectedPriority: "1.315222", }, } replyfn := func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) reply.DistributedTraceTimestampGenerator = func() time.Time { return time.Unix(1577830891, 900000000) } reply.AccountID = "123" reply.TrustedAccountKey = "123" } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { app := testApp(replyfn, cfgFn, t) txn := app.StartTransaction("hello") inboundHdrs := map[string][]string{ DistributedTraceNewRelicHeader: {`{"v":[0,1],"d":{"ty":"App","ap":"456","ac":"123","id":"myid","tr":"mytrip","ti":1574881875872` + tc.incomingSampledAndPriority + "}}", }, } txn.AcceptDistributedTraceHeaders(TransportHTTP, inboundHdrs) outboundHdrs := http.Header{} txn.InsertDistributedTraceHeaders(outboundHdrs) traceState := outboundHdrs.Get(DistributedTraceW3CTraceStateHeader) if traceState != "123@nr=0-0-123--e71870997d57214c-1ae969564b34a33e-1-"+tc.expectedPriority+"-1577830891900" { t.Error(tc.expectedPriority, traceState) } }) } } func TestShouldCollectSpanEvents(t *testing.T) { txn := &txn{} txn.appRun = &appRun{} txn.sampledCalculated = true txn.BetterCAT.Sampled = true txn.BetterCAT.Enabled = true txn.Config.DistributedTracer.Enabled = true txn.Config.SpanEvents.Enabled = true // Success if collect := txn.shouldCollectSpanEvents(); !collect { t.Error(collect) } // Not sampled txn.BetterCAT.Sampled = false if collect := txn.shouldCollectSpanEvents(); collect { t.Error(collect) } txn.BetterCAT.Sampled = true // Span events disabled txn.Config.SpanEvents.Enabled = false if collect := txn.shouldCollectSpanEvents(); collect { t.Error(collect) } txn.Config.SpanEvents.Enabled = true // DT disabled txn.Config.DistributedTracer.Enabled = false if collect := txn.shouldCollectSpanEvents(); collect { t.Error(collect) } txn.Config.DistributedTracer.Enabled = true // Success, validate previous testcases. if collect := txn.shouldCollectSpanEvents(); !collect { t.Error(collect) } } func TestErrorAttrsAddedToSpan(t *testing.T) { app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") s1 := txn.StartSegment("s1") s2 := txn.StartSegment("s2") txn.NoticeError(errors.New("error")) s2.End() s1.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "*errors.errorString", SpanAttributeErrorMessage: "error", }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "parentId": internal.MatchAnything, "name": "Custom/s2", }, }, { AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "parentId": internal.MatchAnything, "name": "Custom/s1", }, }, { AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) } type sampleErrorClass struct{} func (s sampleErrorClass) Error() string { return "Custom error message" } func TestErrorAttrsAreOverwritten(t *testing.T) { app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") s1 := txn.StartSegment("s1") txn.NoticeError(errors.New("error")) txn.NoticeError(sampleErrorClass{}) s1.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "newrelic.sampleErrorClass", SpanAttributeErrorMessage: "Custom error message", }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "parentId": internal.MatchAnything, "name": "Custom/s1", }, }, { AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) } func TestErrMsgDisallowed_ErrorMsgIsNotAdded(t *testing.T) { type testCase struct { name string replyFn func(reply *internal.ConnectReply) cfgFn func(cfg *Config) message string } cases := []testCase{ { name: "High Security enabled", replyFn: replyFn, cfgFn: func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.HighSecurity = true }, message: "message removed by high security setting", }, { name: "Security Policies disallows raw exception messages", replyFn: func(reply *internal.ConnectReply) { reply.SetSampleEverything() reply.SecurityPolicies.AllowRawExceptionMessages.SetEnabled(false) }, cfgFn: cfgFn, message: "message removed by security policy", }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { app := testApp(testCase.replyFn, testCase.cfgFn, t) txn := app.StartTransaction("hello") s1 := txn.StartSegment("s1") txn.NoticeError(sampleErrorClass{}) s1.End() txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "newrelic.sampleErrorClass", SpanAttributeErrorMessage: testCase.message, }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "parentId": internal.MatchAnything, "name": "Custom/s1", }, }, { AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) }) } } func TestErrAttrsAddedToRootSpan(t *testing.T) { app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(sampleErrorClass{}) txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "newrelic.sampleErrorClass", SpanAttributeErrorMessage: "Custom error message", }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) } func TestErrAttrsExcludedFromRootSpan(t *testing.T) { cfgFn = func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Attributes.Exclude = []string{ SpanAttributeErrorClass, SpanAttributeErrorMessage, } } app := testApp(replyFn, cfgFn, t) txn := app.StartTransaction("hello") txn.NoticeError(sampleErrorClass{}) txn.End() app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, }, }) } func TestErrAttrsAddedWhenPanic(t *testing.T) { cfgFnRecordPanics := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.ErrorCollector.RecordPanics = true } app := testApp(replyFn, cfgFnRecordPanics, t) func() { defer func() { if recovered := recover(); recovered == nil { t.Error("code did not panic as expected") } }() txn := app.StartTransaction("hello") defer txn.End() panic("whoopsidoodle") }() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "panic", SpanAttributeErrorMessage: "whoopsidoodle", }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) } func TestPanicNilRecovery(t *testing.T) { cfgFnRecordPanics := func(cfg *Config) { cfg.DistributedTracer.Enabled = true cfg.ErrorCollector.RecordPanics = true } app := testApp(replyFn, cfgFnRecordPanics, t) func() { defer func() { recovered := recover() if recovered == nil { t.Error("code did not panic as expected") } else if _, isNil := recovered.(*runtime.PanicNilError); !isNil { t.Errorf("code did not panic with nil as expected; got %v (type %T) instead", recovered, recovered) } }() txn := app.StartTransaction("hello") defer txn.End() panic(nil) }() app.ExpectSpanEvents(t, []internal.WantEvent{ { AgentAttributes: map[string]interface{}{ SpanAttributeErrorClass: "panic", SpanAttributeErrorMessage: "panic called with nil argument", }, Intrinsics: map[string]interface{}{ "category": internal.MatchAnything, "timestamp": internal.MatchAnything, "name": "OtherTransaction/Go/hello", "transaction.name": "OtherTransaction/Go/hello", "nr.entryPoint": true, }, }, }) } func TestIsEndedInternal(t *testing.T) { tests := []struct { name string txn *txn expected bool }{ { name: "finished transaction", txn: &txn{finished: true}, expected: true, }, { name: "unfinished transaction", txn: &txn{finished: false}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { thread := &thread{txn: tt.txn} result := thread.IsEnded() if result != tt.expected { t.Errorf("IsEnded() = %v; want %v", result, tt.expected) } }) } } go-agent-3.42.0/v3/newrelic/intrinsics.go000066400000000000000000000026161510742411500201620ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" ) const ( expectErrorAttr = "error.expected" ) func addOptionalStringField(w *jsonFieldsWriter, key, value string) { if value != "" { w.stringField(key, value) } } func intrinsicsJSON(e *txnEvent, buf *bytes.Buffer, expect bool) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') w.floatField("totalTime", e.TotalTime.Seconds()) if e.BetterCAT.Enabled { w.stringField("guid", e.BetterCAT.TxnID) w.stringField("traceId", e.BetterCAT.TraceID) w.writerField("priority", e.BetterCAT.Priority) w.boolField("sampled", e.BetterCAT.Sampled) } if expect { w.stringField(expectErrorAttr, "true") } if e.CrossProcess.Used() { addOptionalStringField(&w, "client_cross_process_id", e.CrossProcess.ClientID) addOptionalStringField(&w, "trip_id", e.CrossProcess.TripID) addOptionalStringField(&w, "path_hash", e.CrossProcess.PathHash) addOptionalStringField(&w, "referring_transaction_guid", e.CrossProcess.ReferringTxnGUID) } if e.CrossProcess.IsSynthetics() { addOptionalStringField(&w, "synthetics_resource_id", e.CrossProcess.Synthetics.ResourceID) addOptionalStringField(&w, "synthetics_job_id", e.CrossProcess.Synthetics.JobID) addOptionalStringField(&w, "synthetics_monitor_id", e.CrossProcess.Synthetics.MonitorID) } buf.WriteByte('}') } go-agent-3.42.0/v3/newrelic/json_object_writer.go000066400000000000000000000025721510742411500216710ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "github.com/newrelic/go-agent/v3/internal/jsonx" ) type jsonWriter interface { WriteJSON(buf *bytes.Buffer) } type jsonFieldsWriter struct { buf *bytes.Buffer needsComma bool } func (w *jsonFieldsWriter) addKey(key string) { if w.needsComma { w.buf.WriteByte(',') } else { w.needsComma = true } // defensively assume that the key needs escaping: jsonx.AppendString(w.buf, key) w.buf.WriteByte(':') } func (w *jsonFieldsWriter) stringField(key string, val string) { w.addKey(key) jsonx.AppendString(w.buf, val) } func (w *jsonFieldsWriter) intField(key string, val int64) { w.addKey(key) jsonx.AppendInt(w.buf, val) } func (w *jsonFieldsWriter) floatField(key string, val float64) { w.addKey(key) jsonx.AppendFloat(w.buf, val) } func (w *jsonFieldsWriter) float32Field(key string, val float32) { w.addKey(key) jsonx.AppendFloat32(w.buf, val) } func (w *jsonFieldsWriter) boolField(key string, val bool) { w.addKey(key) if val { w.buf.WriteString("true") } else { w.buf.WriteString("false") } } func (w *jsonFieldsWriter) rawField(key string, val jsonString) { w.addKey(key) w.buf.WriteString(string(val)) } func (w *jsonFieldsWriter) writerField(key string, val jsonWriter) { w.addKey(key) val.WriteJSON(w.buf) } go-agent-3.42.0/v3/newrelic/json_object_writer_test.go000066400000000000000000000010731510742411500227230ustar00rootroot00000000000000package newrelic import ( "bytes" "testing" ) func BenchmarkStringFieldShort(b *testing.B) { writer := jsonFieldsWriter{ buf: bytes.NewBuffer(make([]byte, 300)), } for i := 0; i < b.N; i++ { writer.stringField("testkey", "this is a short string") } } func BenchmarkStringFieldLong(b *testing.B) { writer := jsonFieldsWriter{ buf: bytes.NewBuffer(make([]byte, 300)), } for i := 0; i < b.N; i++ { writer.stringField("testkey", "this is a long string that will capture the runtime performance impact that writing more bytes has on this function") } } go-agent-3.42.0/v3/newrelic/limits.go000066400000000000000000000036101510742411500172710ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "time" const ( // app behavior // fixedHarvestPeriod is the period that fixed period data (metrics, // traces, and span events) is sent to New Relic. fixedHarvestPeriod = 60 * time.Second // collectorTimeout is the timeout used in the client for communication // with New Relic's servers. collectorTimeout = 20 * time.Second // appDataChanSize is the size of the channel that contains data sent // the app processor. appDataChanSize = 200 failedMetricAttemptsLimit = 5 failedEventsAttemptsLimit = 10 // transaction behavior maxStackTraceFrames = 100 // maxTxnErrors is the maximum number of errors captured per // transaction. maxTxnErrors = 5 maxTxnSlowQueries = 10 startingTxnTraceNodes = 16 maxTxnTraceNodes = 256 // harvest data maxMetrics = 2 * 1000 maxRegularTraces = 1 maxSyntheticsTraces = 20 maxHarvestErrors = 20 maxHarvestSlowSQLs = 10 errorEventMessageLengthLimit = 4096 // attributes attributeKeyLengthLimit = 255 attributeValueLengthLimit = 255 attributeUserLimit = 64 attributeSpanDBStatementLimit = 4096 // int representing max db.statement size in bytes // attributeErrorLimit limits the number of extra attributes that can be // provided when noticing an error. attributeErrorLimit = 32 customEventAttributeLimit = 64 // Limits affecting Config validation are found in the config package. // runtimeSamplerPeriod is the period of the runtime sampler. Runtime // metrics should not depend on the sampler period, but the period must // be the same across instances. For that reason, this value should not // be changed without notifying customers that they must update all // instance simultaneously for valid runtime metrics. runtimeSamplerPeriod = 60 * time.Second ) go-agent-3.42.0/v3/newrelic/log.go000066400000000000000000000032101510742411500165450ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "io" "github.com/newrelic/go-agent/v3/internal/logger" ) // Logger is the interface that is used for logging in the Go Agent. Assign // the Config.Logger field to the Logger you wish to use. Loggers must be safe // for use in multiple goroutines. Two Logger implementations are included: // NewLogger, which logs at info level, and NewDebugLogger which logs at debug // level. logrus, logxi, and zap are supported by the integration packages // https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogrus, // https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrlogxi, // https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzap, // and https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrzerolog // respectively. type Logger interface { Error(msg string, context map[string]interface{}) Warn(msg string, context map[string]interface{}) Info(msg string, context map[string]interface{}) Debug(msg string, context map[string]interface{}) DebugEnabled() bool } // NewLogger creates a basic Logger at info level. // // Deprecated: NewLogger is deprecated and will be removed in a future release. // Use the ConfigInfoLogger ConfigOption instead. func NewLogger(w io.Writer) Logger { return logger.New(w, false) } // NewDebugLogger creates a basic Logger at debug level. // // Deprecated: NewDebugLogger is deprecated and will be removed in a future // release. Use the ConfigDebugLogger ConfigOption instead. func NewDebugLogger(w io.Writer) Logger { return logger.New(w, true) } go-agent-3.42.0/v3/newrelic/log_event.go000066400000000000000000000153111510742411500177530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "errors" "fmt" "strings" "time" "github.com/newrelic/go-agent/v3/internal/logcontext" ) const ( // MaxLogLength is the maximum number of bytes the log message is allowed to be MaxLogLength = 32768 ) type logEvent struct { attributes map[string]any priority priority timestamp int64 severity string message string spanID string traceID string } // LogData contains data fields that are needed to generate log events. // Note: if you are passing a struct, map, slice, or array as an attribute, try to pass it as a JSON string generated by the logging framework if possible. // The collector can parse that into an object on New Relic's side. // This is preferable because the json.Marshal method used in the agent to create the string log JSON is usually less efficient than the tools built into // logging products for creating stringified JSON for complex objects and data structures. type LogData struct { Timestamp int64 // Optional: Unix Millisecond Timestamp; A timestamp will be generated if unset Severity string // Optional: Severity of log being consumed Message string // Optional: Message of log being consumed; Maximum size: 32768 Bytes. Attributes map[string]any // Optional: a key value pair with a string key, and any value. This can be used for categorizing logs in the UI. } // writeJSON prepares JSON in the format expected by the collector. func (e *logEvent) WriteJSON(buf *bytes.Buffer) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') w.stringField(logcontext.LogSeverityFieldName, e.severity) w.stringField(logcontext.LogMessageFieldName, e.message) if len(e.spanID) > 0 { w.stringField(logcontext.LogSpanIDFieldName, e.spanID) } if len(e.traceID) > 0 { w.stringField(logcontext.LogTraceIDFieldName, e.traceID) } w.needsComma = false buf.WriteByte(',') w.intField(logcontext.LogTimestampFieldName, e.timestamp) if e.attributes != nil && len(e.attributes) > 0 { buf.WriteString(`,"attributes":{`) w := jsonFieldsWriter{buf: buf} for key, val := range e.attributes { writeAttributeValueJSON(&w, key, val) } buf.WriteByte('}') } buf.WriteByte('}') } // MarshalJSON is used for testing. func (e *logEvent) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, logcontext.AverageLogSizeEstimate)) e.WriteJSON(buf) return buf.Bytes(), nil } var ( errNilLogData = errors.New("log data can not be nil") errLogMessageTooLarge = fmt.Errorf("log message can not exceed %d bytes", MaxLogLength) ) func (data *LogData) toLogEvent() (logEvent, error) { if data == nil { return logEvent{}, errNilLogData } if data.Severity == "" { data.Severity = logcontext.LogSeverityUnknown } if len(data.Message) > MaxLogLength { return logEvent{}, errLogMessageTooLarge } if data.Timestamp == 0 { data.Timestamp = int64(timeToUnixMilliseconds(time.Now())) } data.Message = strings.TrimSpace(data.Message) data.Severity = strings.TrimSpace(data.Severity) event := logEvent{ priority: newPriority(), message: data.Message, severity: data.Severity, timestamp: data.Timestamp, attributes: data.Attributes, } return event, nil } func (e *logEvent) MergeIntoHarvest(h *harvest) { h.LogEvents.Add(e) } const ( logDecorationErrorHeader = "New Relic failed to decorate a log" ) var ( // ErrNilLogBuffer is a type of error that occurs when the New Relic log decorator is passed a nil object when it was // expecting a valid, non nil pointer to a log bytes.Buffer object. ErrNilLogBuffer = fmt.Errorf("%s: the EnrichLog() function must not be passed a nil byte buffer", logDecorationErrorHeader) // ErrNoApplication is a type of error that occurs when the New Relic log decorator is passed a nil New Relic Application // when it was expecting a valid, non nil pointer to a New Relic application. ErrNoApplication = fmt.Errorf("%s: a non nil application or transaction must be provided to enrich a log", logDecorationErrorHeader) ) type logEnricherConfig struct { app *Application txn *Transaction } // EnricherOption is a function that configures the enricher based on the source of data it receives. type EnricherOption func(*logEnricherConfig) // FromApp configures the log enricher to build a linking payload from an application. func FromApp(app *Application) EnricherOption { return func(cfg *logEnricherConfig) { cfg.app = app } } // FromTxn configures the log enricher to build a linking payload from a transaction. func FromTxn(txn *Transaction) EnricherOption { return func(cfg *logEnricherConfig) { cfg.txn = txn } } type linkingMetadata struct { traceID string spanID string entityGUID string hostname string entityName string } // EnrichLog appends newrelic linking metadata to a log stored in a byte buffer. // This should only be used by plugins built for frameworks. func EnrichLog(buf *bytes.Buffer, opts EnricherOption) error { config := logEnricherConfig{} opts(&config) if buf == nil { return ErrNilLogBuffer } md := linkingMetadata{} var app *Application var txn *Transaction if config.app != nil { app = config.app } else if config.txn != nil { app = config.txn.Application() txn = config.txn txnMD := txn.thread.GetTraceMetadata() md.spanID = txnMD.SpanID md.traceID = txnMD.TraceID } else { return ErrNoApplication } if app.app == nil { return ErrNoApplication } reply, err := app.app.getState() if err != nil { app.app.Debug("cannot enrich logs, unable to reach application", map[string]interface{}{"error": err.Error()}) // If the application is shut down, don't return an error so the log can still be written. // If debug logging is enabled, the error will be logged there. if err == errApplicationShutDown { return nil } return err } md.entityGUID = reply.Reply.EntityGUID md.entityName = app.app.config.AppName md.hostname = app.app.config.hostname if reply.Config.ApplicationLogging.Enabled && reply.Config.ApplicationLogging.LocalDecorating.Enabled { md.appendLinkingMetadata(buf) } return nil } func (md *linkingMetadata) appendLinkingMetadata(buf *bytes.Buffer) { if md.entityGUID == "" || md.entityName == "" || md.hostname == "" { return } addDynamicSpacing(buf) buf.WriteString("NR-LINKING|") buf.WriteString(md.entityGUID) buf.WriteByte('|') buf.WriteString(md.hostname) buf.WriteByte('|') buf.WriteString(md.traceID) buf.WriteByte('|') buf.WriteString(md.spanID) buf.WriteByte('|') buf.WriteString(md.entityName) buf.WriteByte('|') } func addDynamicSpacing(buf *bytes.Buffer) { if buf.Len() == 0 { return } bytes := buf.Bytes() if bytes[len(bytes)-1] != ' ' { buf.WriteByte(' ') } } go-agent-3.42.0/v3/newrelic/log_event_test.go000066400000000000000000000163351510742411500210210ustar00rootroot00000000000000package newrelic import ( "bytes" "fmt" "math/rand" "testing" "time" "github.com/newrelic/go-agent/v3/internal/logcontext" "github.com/newrelic/go-agent/v3/internal/sysinfo" ) func TestWriteJSON(t *testing.T) { event := logEvent{ severity: "INFO", message: "test message", timestamp: 123456, } actual, err := event.MarshalJSON() if err != nil { t.Error(err) } expect := `{"level":"INFO","message":"test message","timestamp":123456}` actualString := string(actual) if expect != actualString { t.Errorf("Log json did not build correctly: expecting %s, got %s", expect, actualString) } } func TestToLogEvent(t *testing.T) { type testcase struct { name string data LogData expectEvent logEvent expectErr error skipTimestamp bool } testcases := []testcase{ { name: "context nil", data: LogData{ Timestamp: 123456, Severity: "info", Message: "test 123", }, expectEvent: logEvent{ timestamp: 123456, severity: "info", message: "test 123", }, }, { name: "severity empty", data: LogData{ Timestamp: 123456, Message: "test 123", }, expectEvent: logEvent{ timestamp: 123456, severity: "UNKNOWN", message: "test 123", }, }, { name: "no timestamp", data: LogData{ Severity: "info", Message: "test 123", }, expectEvent: logEvent{ severity: "info", message: "test 123", }, skipTimestamp: true, }, { name: "message too large", data: LogData{ Timestamp: 123456, Severity: "info", Message: randomString(32769), }, expectErr: errLogMessageTooLarge, }, } for _, testcase := range testcases { actualEvent, err := testcase.data.toLogEvent() if testcase.expectErr != err { t.Error(fmt.Errorf("%s: expected error %v, got %v", testcase.name, testcase.expectErr, err)) } if testcase.expectErr == nil { expect := testcase.expectEvent if expect.message != actualEvent.message { t.Error(fmt.Errorf("%s: expected message %s, got %s", testcase.name, expect.message, actualEvent.message)) } if expect.severity != actualEvent.severity { t.Error(fmt.Errorf("%s: expected severity %s, got %s", testcase.name, expect.severity, actualEvent.severity)) } if actualEvent.timestamp == 0 { t.Errorf("timestamp was not set on test %s", testcase.name) } if expect.timestamp != actualEvent.timestamp && !testcase.skipTimestamp { t.Error(fmt.Errorf("%s: expected timestamp %d, got %d", testcase.name, expect.timestamp, actualEvent.timestamp)) } } } } var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randomString(n int) string { b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func TestWriteJSONWithTrace(t *testing.T) { event := logEvent{ severity: "INFO", message: "test message", timestamp: 123456, traceID: "123Ad234", spanID: "adf3441", } actual, err := event.MarshalJSON() if err != nil { t.Error(err) } expect := `{"level":"INFO","message":"test message","span.id":"adf3441","trace.id":"123Ad234","timestamp":123456}` actualString := string(actual) if expect != actualString { t.Errorf("Log json did not build correctly: expecting %s, got %s", expect, actualString) } } func BenchmarkToLogEvent(b *testing.B) { data := LogData{ Timestamp: 123456, Severity: "INFO", Message: "test message", } b.ReportAllocs() for n := 0; n < b.N; n++ { data.toLogEvent() } } func recordLogBenchmarkHelper(b *testing.B, data *LogData, h *harvest) { event, _ := data.toLogEvent() event.MergeIntoHarvest(h) } func BenchmarkRecordLog(b *testing.B) { harvest := newHarvest(time.Now(), testHarvestCfgr) data := LogData{ Timestamp: 123456, Severity: "INFO", Message: "test message", } b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { recordLogBenchmarkHelper(b, &data, harvest) } } func BenchmarkWriteJSON(b *testing.B) { data := LogData{ Timestamp: 123456, Severity: "INFO", Message: "This is a log message that represents an estimate for how long the average log message is. The average log payload is 700 bytes.", } event, err := data.toLogEvent() if err != nil { b.Fail() } buf := bytes.NewBuffer(make([]byte, 0, logcontext.AverageLogSizeEstimate)) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { event.WriteJSON(buf) } } var ( host, _ = sysinfo.Hostname() ) func TestEnrichLogFromApp(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, func(cfg *Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = false cfg.ApplicationLogging.LocalDecorating.Enabled = true }, ) buf := bytes.NewBuffer([]byte{}) EnrichLog(buf, FromApp(testApp.Application)) state, err := testApp.app.getState() if err != nil { t.Fatal(err) } logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ Hostname: host, EntityGUID: state.Reply.EntityGUID, EntityName: testApp.app.config.AppName, }) } func TestEnrichLogFromAppDisabled(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, func(cfg *Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = false cfg.ApplicationLogging.LocalDecorating.Enabled = false }, ) buf := bytes.NewBuffer([]byte{}) EnrichLog(buf, FromApp(testApp.Application)) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ DecorationDisabled: true, }) } func TestEnrichLogFromTxn(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, func(cfg *Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = false cfg.ApplicationLogging.LocalDecorating.Enabled = true }, ) buf := bytes.NewBuffer([]byte{}) txn := testApp.Application.StartTransaction("test transaction") defer txn.End() EnrichLog(buf, FromTxn(txn)) state, err := testApp.app.getState() if err != nil { t.Fatal(err) } logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ Hostname: host, EntityGUID: state.Reply.EntityGUID, EntityName: testApp.app.config.AppName, TraceID: txn.GetLinkingMetadata().TraceID, SpanID: txn.GetLinkingMetadata().SpanID, }) } func TestEnrichLogFromTxnDisabled(t *testing.T) { testApp := newTestApp( sampleEverythingReplyFn, func(cfg *Config) { cfg.Enabled = false cfg.ApplicationLogging.Enabled = true cfg.ApplicationLogging.Forwarding.Enabled = false cfg.ApplicationLogging.LocalDecorating.Enabled = false }, ) buf := bytes.NewBuffer([]byte{}) txn := testApp.Application.StartTransaction("test transaction") defer txn.End() EnrichLog(buf, FromTxn(txn)) logcontext.ValidateDecoratedOutput(t, buf, &logcontext.DecorationExpect{ DecorationDisabled: true, }) } func BenchmarkAppendLinkingMetadata(b *testing.B) { buf := bytes.NewBuffer([]byte("test log message")) md := linkingMetadata{ traceID: "testTraceID", spanID: "testSpanID", entityGUID: "testEntityGUID", hostname: "testHostname", entityName: "testEntityName", } b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { md.appendLinkingMetadata(buf) } } go-agent-3.42.0/v3/newrelic/log_events.go000066400000000000000000000164661510742411500201520ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "container/heap" "slices" "strings" "time" "github.com/newrelic/go-agent/v3/internal/jsonx" "github.com/newrelic/go-agent/v3/internal/logcontext" ) type commonAttributes struct { entityGUID string entityName string hostname string } type logEvents struct { numSeen int failedHarvests int severityCount map[string]int commonAttributes config loggingConfig logs logEventHeap } // NumSeen returns the number of events seen func (events *logEvents) NumSeen() float64 { return float64(events.numSeen) } // NumSaved returns the number of events that will be harvested for this cycle func (events *logEvents) NumSaved() float64 { return float64(len(events.logs)) } // Adds logging metrics to a harvest metric table if appropriate func (events *logEvents) RecordLoggingMetrics(metrics *metricTable) { // This is done to avoid accessing locks 3 times instead of once seen := events.NumSeen() saved := events.NumSaved() if events.config.collectMetrics && metrics != nil { metrics.addCount(logsSeen, seen, forced) for k, v := range events.severityCount { severitySeen := logsSeen + "/" + k metrics.addCount(severitySeen, float64(v), forced) } } if events.config.collectEvents { metrics.addCount(logsDropped, seen-saved, forced) } } type logEventHeap []logEvent // TODO: when go 1.18 becomes the minimum supported version, re-write to make a generic heap implementation // for all event heaps, to de-duplicate this code // func (events *logEvents) func (h logEventHeap) Len() int { return len(h) } func (h logEventHeap) Less(i, j int) bool { return h[i].priority.isLowerPriority(h[j].priority) } func (h logEventHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } // To avoid using interface reflection, this function is used in place of Push() to add log events to the heap // Please replace all of this when the minimum supported version of go is 1.18 so that we can use generics func (h *logEventHeap) Add(event *logEvent) { // when fewer events are in the heap than the capacity, do not heap sort if len(*h) < cap(*h) { // copy log event onto event heap *h = append(*h, *event) if len(*h) == cap(*h) { // Delay heap initialization so that we can have // deterministic ordering for integration tests (the max // is not being reached). heap.Init(*h) } return } if event.priority.isLowerPriority((*h)[0].priority) { return } (*h)[0] = *event heap.Fix(h, 0) } // Push and Pop are unused: only heap.Init and heap.Fix are used. func (h logEventHeap) Pop() interface{} { return nil } func (h logEventHeap) Push(x interface{}) {} func newLogEvents(ca commonAttributes, loggingConfig loggingConfig) *logEvents { return &logEvents{ commonAttributes: ca, config: loggingConfig, severityCount: map[string]int{}, logs: make(logEventHeap, 0, loggingConfig.maxLogEvents), } } func (events *logEvents) capacity() int { return events.config.maxLogEvents } func (events *logEvents) Add(e *logEvent) { // always collect this but do not report logging metrics when disabled events.numSeen++ events.severityCount[e.severity]++ // Do not collect log events when the harvest capacity is intentionally set to 0 // or the collection of events is explicitly disabled if events.capacity() == 0 || !events.config.collectEvents { // Configurable event harvest limits may be zero. return } // Add logs to event heap events.logs.Add(e) } func (events *logEvents) mergeFailed(other *logEvents) { fails := other.failedHarvests + 1 if fails >= failedEventsAttemptsLimit { return } events.failedHarvests = fails events.Merge(other) } // Merge two logEvents together func (events *logEvents) Merge(other *logEvents) { allSeen := events.NumSeen() + other.NumSeen() for _, e := range other.logs { events.Add(&e) } events.numSeen = int(allSeen) } func (events *logEvents) CollectorJSON(agentRunID string) ([]byte, error) { if len(events.logs) == 0 { return nil, nil } estimate := logcontext.AverageLogSizeEstimate * len(events.logs) buf := bytes.NewBuffer(make([]byte, 0, estimate)) if events.numSeen == 0 { return nil, nil } buf.WriteByte('[') buf.WriteByte('{') buf.WriteString(`"common":`) buf.WriteByte('{') buf.WriteString(`"attributes":`) buf.WriteByte('{') buf.WriteString(`"entity.guid":`) jsonx.AppendString(buf, events.entityGUID) buf.WriteByte(',') buf.WriteString(`"entity.name":`) jsonx.AppendString(buf, events.entityName) buf.WriteByte(',') buf.WriteString(`"hostname":`) jsonx.AppendString(buf, events.hostname) if events.config.includeLabels != nil { for k, v := range events.config.includeLabels { if events.config.excludeLabels == nil || !slices.ContainsFunc(*events.config.excludeLabels, func(s string) bool { return strings.ToLower(s) == strings.ToLower(k) }) { buf.WriteByte(',') jsonx.AppendString(buf, "tags."+k) buf.WriteByte(':') jsonx.AppendString(buf, v) } } } if events.config.customAttributes != nil { for k, v := range events.config.customAttributes { buf.WriteByte(',') jsonx.AppendString(buf, k) buf.WriteByte(':') jsonx.AppendString(buf, v) } } buf.WriteByte('}') buf.WriteByte('}') buf.WriteByte(',') buf.WriteString(`"logs":`) buf.WriteByte('[') for i, e := range events.logs { // If severity is empty string, then this is not a user provided entry, and is empty. // Do not write json to buffer in this case. if e.severity != "" { e.WriteJSON(buf) if i != len(events.logs)-1 { buf.WriteByte(',') } } } buf.WriteByte(']') buf.WriteByte('}') buf.WriteByte(']') return buf.Bytes(), nil } // split splits the events into two. NOTE! The two event pools are not valid // priority queues, and should only be used to create JSON, not for adding any // events. func (events *logEvents) split() (*logEvents, *logEvents) { // numSeen is conserved: e1.numSeen + e2.numSeen == events.numSeen. sc1, sc2 := splitSeverityCount(events.severityCount) e1 := &logEvents{ numSeen: len(events.logs) / 2, failedHarvests: events.failedHarvests / 2, severityCount: sc1, commonAttributes: events.commonAttributes, logs: make([]logEvent, len(events.logs)/2), } e2 := &logEvents{ numSeen: events.numSeen - e1.numSeen, failedHarvests: events.failedHarvests - e1.failedHarvests, severityCount: sc2, commonAttributes: events.commonAttributes, logs: make([]logEvent, len(events.logs)-len(e1.logs)), } // Note that slicing is not used to ensure that length == capacity for // e1.events and e2.events. copy(e1.logs, events.logs) copy(e2.logs, events.logs[len(events.logs)/2:]) return e1, e2 } // splits the contents and counts of the severity map func splitSeverityCount(severityCount map[string]int) (map[string]int, map[string]int) { count1 := map[string]int{} count2 := map[string]int{} for k, v := range severityCount { count1[k] = v / 2 count2[k] = v - count1[k] } return count1, count2 } func (events *logEvents) MergeIntoHarvest(h *harvest) { h.LogEvents.mergeFailed(events) } func (events *logEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return events.CollectorJSON(agentRunID) } func (events *logEvents) EndpointMethod() string { return cmdLogEvents } go-agent-3.42.0/v3/newrelic/log_events_test.go000066400000000000000000000371151510742411500212030ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "fmt" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) var ( testGUID = "testGUID" testEntityName = "testEntityName" testHostname = "testHostname" testCommonAttributes = commonAttributes{ entityGUID: testGUID, entityName: testEntityName, hostname: testHostname, } commonJSON = `[{"common":{"attributes":{"entity.guid":"testGUID","entity.name":"testEntityName","hostname":"testHostname"}},"logs":[` infoLevel = "INFO" ) func loggingConfigEnabled(limit int) loggingConfig { return loggingConfig{ loggingEnabled: true, localEnrichment: true, collectEvents: true, collectMetrics: true, maxLogEvents: limit, } } func sampleLogEvent(priority priority, severity, message string, attributes map[string]any) *logEvent { return &logEvent{ priority: priority, severity: severity, message: message, attributes: attributes, timestamp: 123456, } } func TestBasicLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(5)) events.Add(sampleLogEvent(0.5, infoLevel, "message1", nil)) events.Add(sampleLogEvent(0.5, infoLevel, "message2", nil)) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } expected := commonJSON + `{"level":"INFO","message":"message1","timestamp":123456},` + `{"level":"INFO","message":"message2","timestamp":123456}]}]` if string(json) != expected { t.Error(string(json), expected) } if events.numSeen != 2 { t.Error(events.numSeen) } if events.NumSaved() != 2 { t.Error(events.NumSaved()) } } type testStruct struct { A string B int C c } type c struct { D string } func TestBasicLogEventWithAttributes(t *testing.T) { st := testStruct{ A: "a", B: 1, C: c{"hello"}, } events := newLogEvents(testCommonAttributes, loggingConfigEnabled(6)) events.Add(sampleLogEvent(0.5, infoLevel, "message1", map[string]any{"two": "hi"})) events.Add(sampleLogEvent(0.5, infoLevel, "message2", map[string]any{"struct": st})) events.Add(sampleLogEvent(0.5, infoLevel, "message3", map[string]any{"map": map[string]string{"hi": "hello"}})) events.Add(sampleLogEvent(0.5, infoLevel, "message4", map[string]any{"slice": []string{"hi", "hello", "test"}})) events.Add(sampleLogEvent(0.5, infoLevel, "message5", map[string]any{"array": [2]int{1, 2}})) events.Add(sampleLogEvent(0.5, infoLevel, "message6", map[string]any{"error": errors.New("test error")})) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } expected := commonJSON + `{"level":"INFO","message":"message1","timestamp":123456,"attributes":{"two":"hi"}},` + `{"level":"INFO","message":"message2","timestamp":123456,"attributes":{"struct":"{\"A\":\"a\",\"B\":1,\"C\":{\"D\":\"hello\"}}"}},` + `{"level":"INFO","message":"message3","timestamp":123456,"attributes":{"map":"{\"hi\":\"hello\"}"}},` + `{"level":"INFO","message":"message4","timestamp":123456,"attributes":{"slice":"[\"hi\",\"hello\",\"test\"]"}},` + `{"level":"INFO","message":"message5","timestamp":123456,"attributes":{"array":"[1,2]"}},` + `{"level":"INFO","message":"message6","timestamp":123456,"attributes":{"error":"test error"}}]}]` if string(json) != expected { t.Error("actual not equal to expected:\n", string(json), "\n", expected) } if events.numSeen != 6 { t.Error(events.numSeen) } if events.NumSaved() != 6 { t.Error(events.NumSaved()) } } func TestEmptyLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if nil != json { t.Error(string(json)) } if events.numSeen != 0 { t.Error(events.numSeen) } if events.NumSaved() != 0 { t.Error(events.NumSaved()) } } // The events with the highest priority should make it: a, c, e func TestSamplingLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) events.Add(sampleLogEvent(0.999999, infoLevel, "a", nil)) events.Add(sampleLogEvent(0.1, infoLevel, "b", nil)) events.Add(sampleLogEvent(0.9, infoLevel, "c", nil)) events.Add(sampleLogEvent(0.2, infoLevel, "d", nil)) events.Add(sampleLogEvent(0.8, infoLevel, "e", nil)) events.Add(sampleLogEvent(0.3, infoLevel, "f", nil)) json, err := events.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } expect := commonJSON + `{"level":"INFO","message":"e","timestamp":123456},` + `{"level":"INFO","message":"a","timestamp":123456},` + `{"level":"INFO","message":"c","timestamp":123456}]}` + `]` if string(json) != expect { t.Error(string(json), expect) } if 6 != events.numSeen { t.Error(events.numSeen) } if 3 != events.NumSaved() { t.Error(events.NumSaved()) } } func TestMergeEmptyLogEvents(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) e1.Merge(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } if nil != json { t.Error(string(json)) } if 0 != e1.numSeen { t.Error(e1.numSeen) } if 0 != e1.NumSaved() { t.Error(e1.NumSaved()) } } func TestMergeFullLogEvents(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e1.Merge(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } // expect the highest priority events: c, g expect := commonJSON + `{"level":"INFO","message":"g","timestamp":123456},` + `{"level":"INFO","message":"c","timestamp":123456}]}]` if string(json) != expect { t.Error(string(json)) } if e1.numSeen != 7 { t.Error(e1.numSeen) } if e1.NumSaved() != 2 { t.Error(e1.NumSaved()) } } func TestLogEventMergeFailedSuccess(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e1.mergeFailed(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } // expect the highest priority events: c, g expect := commonJSON + `{"level":"INFO","message":"g","timestamp":123456},` + `{"level":"INFO","message":"c","timestamp":123456}]}]` if string(json) != expect { t.Error(string(json)) } if 7 != e1.numSeen { t.Error(e1.numSeen) } if 2 != e1.NumSaved() { t.Error(e1.NumSaved()) } if 1 != e1.failedHarvests { t.Error(e1.failedHarvests) } } func TestLogEventMergeFailedLimitReached(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e2.failedHarvests = failedEventsAttemptsLimit e1.mergeFailed(e2) json, err := e1.CollectorJSON(agentRunID) if nil != err { t.Fatal(err) } expect := commonJSON + `{"level":"INFO","message":"b","timestamp":123456},` + `{"level":"INFO","message":"c","timestamp":123456}]}]` if string(json) != expect { t.Error(string(json)) } if 3 != e1.numSeen { t.Error(e1.numSeen) } if 2 != e1.NumSaved() { t.Error(e1.NumSaved()) } if 0 != e1.failedHarvests { t.Error(e1.failedHarvests) } } func TestLogEventsSplitFull(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 15; i++ { priority := priority(float32(i) / 10.0) events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } // Test that the capacity cannot exceed the max. if 10 != events.capacity() { t.Error(events.capacity()) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } expect1 := commonJSON + `{"level":"INFO","message":"0.5","timestamp":123456},` + `{"level":"INFO","message":"0.7","timestamp":123456},` + `{"level":"INFO","message":"0.6","timestamp":123456},` + `{"level":"INFO","message":"0.8","timestamp":123456},` + `{"level":"INFO","message":"0.9","timestamp":123456}]}]` if string(j1) != expect1 { t.Error(string(j1)) } expect2 := commonJSON + `{"level":"INFO","message":"1.1","timestamp":123456},` + `{"level":"INFO","message":"1.4","timestamp":123456},` + `{"level":"INFO","message":"1","timestamp":123456},` + `{"level":"INFO","message":"1.3","timestamp":123456},` + `{"level":"INFO","message":"1.2","timestamp":123456}]}]` if string(j2) != expect2 { t.Error(string(j2)) } } // TODO: When miniumu supported go version is 1.18, make an event heap in GO generics and remove all this duplicate code // interfaces are too slow :( func TestLogEventsSplitNotFullOdd(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 7; i++ { priority := priority(float32(i) / 10.0) events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } expect1 := commonJSON + `{"level":"INFO","message":"0","timestamp":123456},` + `{"level":"INFO","message":"0.1","timestamp":123456},` + `{"level":"INFO","message":"0.2","timestamp":123456}]}]` if string(j1) != expect1 { t.Error(string(j1)) } expect2 := commonJSON + `{"level":"INFO","message":"0.3","timestamp":123456},` + `{"level":"INFO","message":"0.4","timestamp":123456},` + `{"level":"INFO","message":"0.5","timestamp":123456},` + `{"level":"INFO","message":"0.6","timestamp":123456}]}]` if string(j2) != expect2 { t.Error(string(j2)) } } func TestLogEventsSplitNotFullEven(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 8; i++ { priority := priority(float32(i) / 10.0) events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) j2, err2 := e2.CollectorJSON(agentRunID) if err1 != nil || err2 != nil { t.Fatal(err1, err2) } expect1 := commonJSON + `{"level":"INFO","message":"0","timestamp":123456},` + `{"level":"INFO","message":"0.1","timestamp":123456},` + `{"level":"INFO","message":"0.2","timestamp":123456},` + `{"level":"INFO","message":"0.3","timestamp":123456}]}]` if string(j1) != expect1 { t.Error(string(j1)) } expect2 := commonJSON + `{"level":"INFO","message":"0.4","timestamp":123456},` + `{"level":"INFO","message":"0.5","timestamp":123456},` + `{"level":"INFO","message":"0.6","timestamp":123456},` + `{"level":"INFO","message":"0.7","timestamp":123456}]}]` if string(j2) != expect2 { t.Error(string(j2)) } } func TestLogEventsZeroCapacity(t *testing.T) { // Analytics events methods should be safe when configurable harvest // settings have an event limit of zero. events := newLogEvents(testCommonAttributes, loggingConfigEnabled(0)) if 0 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } events.Add(sampleLogEvent(0.5, "INFO", "TEST", nil)) if 1 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } js, err := events.CollectorJSON("agentRunID") if err != nil || js != nil { t.Error(err, string(js)) } } func TestLogEventCollectionDisabled(t *testing.T) { // Analytics events methods should be safe when configurable harvest // settings have an event limit of zero. config := loggingConfigEnabled(5) config.collectEvents = false events := newLogEvents(testCommonAttributes, config) if 0 != events.NumSeen() || 0 != len(events.severityCount) || 0 != events.NumSaved() || 5 != events.capacity() { t.Error(events.NumSeen(), len(events.severityCount), events.NumSaved(), events.capacity()) } events.Add(sampleLogEvent(0.5, "INFO", "TEST", nil)) if 1 != events.NumSeen() || 1 != len(events.severityCount) || 0 != events.NumSaved() || 5 != events.capacity() { t.Error(events.NumSeen(), len(events.severityCount), events.NumSaved(), events.capacity()) } js, err := events.CollectorJSON("agentRunID") if err != nil || js != nil { t.Error(err, string(js)) } } func BenchmarkLogEventsAdd(b *testing.B) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(internal.MaxLogEvents)) event := &logEvent{ priority: newPriority(), timestamp: 123456, severity: "INFO", message: "test message", spanID: "Ad300dra7re89", traceID: "2234iIhfLlejrJ0", } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { events.Add(event) } } func BenchmarkLogEventsCollectorJSON(b *testing.B) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(internal.MaxLogEvents)) for i := 0; i < internal.MaxLogEvents; i++ { event := &logEvent{ priority: newPriority(), timestamp: 123456, severity: "INFO", message: "This is a log message that represents an estimate for how long the average log message is. The average log payload is 700 bytese.", spanID: "Ad300dra7re89", traceID: "2234iIhfLlejrJ0", } events.Add(event) } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { js, err := events.CollectorJSON(agentRunID) if nil != err { b.Fatal(err, js) } } } func BenchmarkLogEventCollectorJSON_OneEvent(b *testing.B) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(internal.MaxLogEvents)) event := &logEvent{ priority: newPriority(), timestamp: 123456, severity: "INFO", message: "This is a log message that represents an estimate for how long the average log message is. The average log payload is 700 bytes.", spanID: "Ad300dra7re89", traceID: "2234iIhfLlejrJ0", } events.Add(event) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { js, err := events.CollectorJSON(agentRunID) if nil != err { b.Fatal(err, js) } } } func BenchmarkRecordLoggingMetrics(b *testing.B) { now := time.Now() fixedHarvestTypes := harvestMetricsTraces & harvestTxnEvents & harvestSpanEvents & harvestLogEvents h := newHarvest(now, harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{ fixedHarvestTypes: fixedHarvestPeriod, harvestLogEvents: time.Second * 5, }, LoggingConfig: loggingConfigEnabled(3), }) for i := 0; i < internal.MaxLogEvents; i++ { logEvent := logEvent{ nil, newPriority(), 123456, "INFO", fmt.Sprintf("User 'xyz' logged in %d", i), "123456789ADF", "ADF09876565", } h.LogEvents.Add(&logEvent) } b.ResetTimer() for i := 0; i < b.N; i++ { b.ReportAllocs() h.LogEvents.RecordLoggingMetrics(h.Metrics) } } go-agent-3.42.0/v3/newrelic/metric_names.go000066400000000000000000000345251510742411500204470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "fmt" const ( apdexRollup = "Apdex" apdexPrefix = "Apdex/" webRollup = "WebTransaction" backgroundRollup = "OtherTransaction/all" // https://source.datanerd.us/agents/agent-specs/blob/master/Total-Time-Async.md totalTimeWeb = "WebTransactionTotalTime" totalTimeBackground = "OtherTransactionTotalTime" errorsPrefix = "Errors/" // "HttpDispatcher" metric is used for the overview graph, and // therefore should only be made for web transactions. dispatcherMetric = "HttpDispatcher" queueMetric = "WebFrontend/QueueTime" // Transaction name prefixes are located in connect_reply.go. instanceReporting = "Instance/Reporting" // https://newrelic.atlassian.net/wiki/display/eng/Custom+Events+in+New+Relic+Agents customEventsSeen = "Supportability/Events/Customer/Seen" customEventsSent = "Supportability/Events/Customer/Sent" // https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Events-PORTED.md txnEventsSeen = "Supportability/AnalyticsEvents/TotalEventsSeen" txnEventsSent = "Supportability/AnalyticsEvents/TotalEventsSent" // https://source.datanerd.us/agents/agent-specs/blob/master/Error-Events.md errorEventsSeen = "Supportability/Events/TransactionError/Seen" errorEventsSent = "Supportability/Events/TransactionError/Sent" // https://source.datanerd.us/agents/agent-specs/blob/master/Span-Events.md spanEventsSeen = "Supportability/SpanEvent/TotalEventsSeen" spanEventsSent = "Supportability/SpanEvent/TotalEventsSent" supportabilityDropped = "Supportability/MetricsDropped" // Runtime/System Metrics memoryPhysical = "Memory/Physical" heapObjectsAllocated = "Memory/Heap/AllocatedObjects" cpuUserUtilization = "CPU/User/Utilization" cpuSystemUtilization = "CPU/System/Utilization" cpuUserTime = "CPU/User Time" cpuSystemTime = "CPU/System Time" runGoroutine = "Go/Runtime/Goroutines" gcPauseFraction = "GC/System/Pause Fraction" gcPauses = "GC/System/Pauses" // Configurable event harvest supportability metrics supportReportPeriod = "Supportability/EventHarvest/ReportPeriod" supportTxnEventLimit = "Supportability/EventHarvest/AnalyticEventData/HarvestLimit" supportCustomEventLimit = "Supportability/EventHarvest/CustomEventData/HarvestLimit" supportErrorEventLimit = "Supportability/EventHarvest/ErrorEventData/HarvestLimit" supportSpanEventLimit = "Supportability/EventHarvest/SpanEventData/HarvestLimit" supportLogEventLimit = "Supportability/EventHarvest/LogEventData/HarvestLimit" // Logging Metrics https://source.datanerd.us/agents/agent-specs/pull/570/files // User Facing logsSeen = "Logging/lines" logsDropped = "Logging/Forwarding/Dropped" // Supportability (at connect) supportLogging = "Supportability/Logging/Golang" supportLoggingMetrics = "Supportability/Logging/Metrics/Golang" supportLogForwarding = "Supportability/Logging/Forwarding/Golang" supportLogDecorating = "Supportability/Logging/LocalDecorating/Golang" // Supportability (once per harvest) logEventsSeen = "Supportability/Logging/Forwarding/Seen" logEventsSent = "Supportability/Logging/Forwarding/Sent" ) func supportMetric(metrics *metricTable, b bool, metricName string) { if b { metrics.addSingleCount(metricName, forced) } } // logSupport contains final configuration settings for // logging features for log data generation and supportability // metrics generation. type loggingConfig struct { loggingEnabled bool // application logging features are enabled collectEvents bool // collection of log event data is enabled collectMetrics bool // collection of log metric data is enabled localEnrichment bool // local log enrichment is enabled maxLogEvents int // maximum number of log events allowed to be collected includeLabels map[string]string // READ ONLY: if not nil, add these labels to log common data too excludeLabels *[]string // READ ONLY: if not nil, exclude these label keys from the included labels customAttributes map[string]string // READ ONLY: if not nil, add these custom attributes to log common data too } // Logging metrics that are generated at connect response func (cfg loggingConfig) connectMetrics(ms *metricTable) { supportMetric(ms, cfg.loggingEnabled, supportLogging) supportMetric(ms, cfg.collectEvents, supportLogForwarding) supportMetric(ms, cfg.collectMetrics, supportLoggingMetrics) supportMetric(ms, cfg.localEnrichment, supportLogDecorating) } func loggingFrameworkMetric(ms *metricTable, framework string) { name := fmt.Sprintf("%s/%s", supportLogging, framework) supportMetric(ms, true, name) } // distributedTracingSupport is used to track distributed tracing activity for // supportability. type distributedTracingSupport struct { // New Relic DT fields AcceptPayloadSuccess bool // AcceptPayload was called successfully AcceptPayloadException bool // AcceptPayload had a generic exception AcceptPayloadParseException bool // AcceptPayload had a parsing exception AcceptPayloadCreateBeforeAccept bool // AcceptPayload was ignored because CreatePayload had already been called AcceptPayloadIgnoredMultiple bool // AcceptPayload was ignored because AcceptPayload had already been called AcceptPayloadIgnoredVersion bool // AcceptPayload was ignored because the payload's major version was greater than the agent's AcceptPayloadUntrustedAccount bool // AcceptPayload was ignored because the payload was untrusted AcceptPayloadNullPayload bool // AcceptPayload was ignored because the payload was nil CreatePayloadSuccess bool // CreatePayload was called successfully CreatePayloadException bool // CreatePayload had a generic exception // W3C Trace Context fields TraceContextAcceptSuccess bool // The agent successfully accepted inbound traceparent and tracestate headers. TraceContextAcceptException bool // A generic exception occurred unrelated to parsing while accepting either payload. TraceContextParentParseException bool // The inbound traceparent header could not be parsed. TraceContextStateParseException bool // The inbound tracestate header could not be parsed. TraceContextStateInvalidNrEntry bool // The inbound tracestate header exists, and was accepted, but the New Relic entry was invalid. TraceContextStateNoNrEntry bool // The traceparent header exists, and was accepted, but the tracestate header did not contain a trusted New Relic entry. TraceContextCreateSuccess bool // The agent successfully created the outbound payloads. TraceContextCreateException bool // A generic exception occurred while creating the outbound payloads. } func (dts distributedTracingSupport) isEmpty() bool { return (distributedTracingSupport{}) == dts } func (dts distributedTracingSupport) createMetrics(ms *metricTable) { // Distributed Tracing Supportability Metrics supportMetric(ms, dts.AcceptPayloadSuccess, "Supportability/DistributedTrace/AcceptPayload/Success") supportMetric(ms, dts.AcceptPayloadException, "Supportability/DistributedTrace/AcceptPayload/Exception") supportMetric(ms, dts.AcceptPayloadParseException, "Supportability/DistributedTrace/AcceptPayload/ParseException") supportMetric(ms, dts.AcceptPayloadCreateBeforeAccept, "Supportability/DistributedTrace/AcceptPayload/Ignored/CreateBeforeAccept") supportMetric(ms, dts.AcceptPayloadIgnoredMultiple, "Supportability/DistributedTrace/AcceptPayload/Ignored/Multiple") supportMetric(ms, dts.AcceptPayloadIgnoredVersion, "Supportability/DistributedTrace/AcceptPayload/Ignored/MajorVersion") supportMetric(ms, dts.AcceptPayloadUntrustedAccount, "Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount") supportMetric(ms, dts.AcceptPayloadNullPayload, "Supportability/DistributedTrace/AcceptPayload/Ignored/Null") supportMetric(ms, dts.CreatePayloadSuccess, "Supportability/DistributedTrace/CreatePayload/Success") supportMetric(ms, dts.CreatePayloadException, "Supportability/DistributedTrace/CreatePayload/Exception") // W3C Trace Context Supportability Metrics supportMetric(ms, dts.TraceContextAcceptSuccess, "Supportability/TraceContext/Accept/Success") supportMetric(ms, dts.TraceContextAcceptException, "Supportability/TraceContext/Accept/Exception") supportMetric(ms, dts.TraceContextParentParseException, "Supportability/TraceContext/TraceParent/Parse/Exception") supportMetric(ms, dts.TraceContextStateParseException, "Supportability/TraceContext/TraceState/Parse/Exception") supportMetric(ms, dts.TraceContextCreateSuccess, "Supportability/TraceContext/Create/Success") supportMetric(ms, dts.TraceContextCreateException, "Supportability/TraceContext/Create/Exception") supportMetric(ms, dts.TraceContextStateInvalidNrEntry, "Supportability/TraceContext/TraceState/InvalidNrEntry") supportMetric(ms, dts.TraceContextStateNoNrEntry, "Supportability/TraceContext/TraceState/NoNrEntry") } type rollupMetric struct { all string allWeb string allOther string } func newRollupMetric(s string) rollupMetric { return rollupMetric{ all: s + "all", allWeb: s + "allWeb", allOther: s + "allOther", } } func (r rollupMetric) webOrOther(isWeb bool) string { if isWeb { return r.allWeb } return r.allOther } var ( errorsRollupMetric = newRollupMetric("Errors/") expectedErrorsRollupMetric = newRollupMetric("ErrorsExpected/") // source.datanerd.us/agents/agent-specs/blob/master/APIs/external_segment.md // source.datanerd.us/agents/agent-specs/blob/master/APIs/external_cat.md // source.datanerd.us/agents/agent-specs/blob/master/Cross-Application-Tracing-PORTED.md externalRollupMetric = newRollupMetric("External/") // source.datanerd.us/agents/agent-specs/blob/master/Datastore-Metrics-PORTED.md datastoreRollupMetric = newRollupMetric("Datastore/") datastoreProductMetricsCache = map[string]rollupMetric{ "Cassandra": newRollupMetric("Datastore/Cassandra/"), "Derby": newRollupMetric("Datastore/Derby/"), "Elasticsearch": newRollupMetric("Datastore/Elasticsearch/"), "Firebird": newRollupMetric("Datastore/Firebird/"), "IBMDB2": newRollupMetric("Datastore/IBMDB2/"), "Informix": newRollupMetric("Datastore/Informix/"), "Memcached": newRollupMetric("Datastore/Memcached/"), "MongoDB": newRollupMetric("Datastore/MongoDB/"), "MySQL": newRollupMetric("Datastore/MySQL/"), "MSSQL": newRollupMetric("Datastore/MSSQL/"), "Oracle": newRollupMetric("Datastore/Oracle/"), "Postgres": newRollupMetric("Datastore/Postgres/"), "Redis": newRollupMetric("Datastore/Redis/"), "Solr": newRollupMetric("Datastore/Solr/"), "SQLite": newRollupMetric("Datastore/SQLite/"), "CouchDB": newRollupMetric("Datastore/CouchDB/"), "Riak": newRollupMetric("Datastore/Riak/"), "VoltDB": newRollupMetric("Datastore/VoltDB/"), } ) func customSegmentMetric(s string) string { return "Custom/" + s } // customMetricName is used to construct custom metrics from the input given to // Application.RecordCustomMetric. Note that the "Custom/" prefix helps prevent // collision with other agent metrics, but does not eliminate the possibility // since "Custom/" is also used for segments. func customMetricName(customerInput string) string { return "Custom/" + customerInput } // datastoreMetricKey contains the fields by which datastore metrics are // aggregated. type datastoreMetricKey struct { Product string Collection string Operation string Host string PortPathOrID string } type externalMetricKey struct { Host string Library string Method string ExternalCrossProcessID string ExternalTransactionName string } func datastoreScopedMetric(key datastoreMetricKey) string { if "" != key.Collection { return datastoreStatementMetric(key) } return datastoreOperationMetric(key) } // Datastore/{datastore}/* func datastoreProductMetric(key datastoreMetricKey) rollupMetric { d, ok := datastoreProductMetricsCache[key.Product] if ok { return d } return newRollupMetric("Datastore/" + key.Product + "/") } // Datastore/operation/{datastore}/{operation} func datastoreOperationMetric(key datastoreMetricKey) string { return "Datastore/operation/" + key.Product + "/" + key.Operation } // Datastore/statement/{datastore}/{table}/{operation} func datastoreStatementMetric(key datastoreMetricKey) string { return "Datastore/statement/" + key.Product + "/" + key.Collection + "/" + key.Operation } // Datastore/instance/{datastore}/{host}/{port_path_or_id} func datastoreInstanceMetric(key datastoreMetricKey) string { return "Datastore/instance/" + key.Product + "/" + key.Host + "/" + key.PortPathOrID } func (key externalMetricKey) scopedMetric() string { if "" != key.ExternalCrossProcessID && "" != key.ExternalTransactionName { return externalTransactionMetric(key) } if key.Method == "" { // External/{host}/{library} return "External/" + key.Host + "/" + key.Library } // External/{host}/{library}/{method} return "External/" + key.Host + "/" + key.Library + "/" + key.Method } // External/{host}/all func externalHostMetric(key externalMetricKey) string { return "External/" + key.Host + "/all" } // ExternalApp/{host}/{external_id}/all func externalAppMetric(key externalMetricKey) string { return "ExternalApp/" + key.Host + "/" + key.ExternalCrossProcessID + "/all" } // ExternalTransaction/{host}/{external_id}/{external_txnname} func externalTransactionMetric(key externalMetricKey) string { return "ExternalTransaction/" + key.Host + "/" + key.ExternalCrossProcessID + "/" + key.ExternalTransactionName } func callerFields(c payloadCaller) string { return "/" + c.Type + "/" + c.Account + "/" + c.App + "/" + c.TransportType + "/" } // DurationByCaller/{type}/{account}/{app}/{transport}/* func durationByCallerMetric(c payloadCaller) rollupMetric { return newRollupMetric("DurationByCaller" + callerFields(c)) } // ErrorsByCaller/{type}/{account}/{app}/{transport}/* func errorsByCallerMetric(c payloadCaller) rollupMetric { return newRollupMetric("ErrorsByCaller" + callerFields(c)) } // TransportDuration/{type}/{account}/{app}/{transport}/* func transportDurationMetric(c payloadCaller) rollupMetric { return newRollupMetric("TransportDuration" + callerFields(c)) } go-agent-3.42.0/v3/newrelic/metrics.go000066400000000000000000000146271510742411500174500ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/jsonx" ) type metricForce int const ( forced metricForce = iota unforced ) type metricID struct { Name string `json:"name"` Scope string `json:"scope,omitempty"` } type metricData struct { // These values are in the units expected by the collector. countSatisfied float64 // Seconds, or count for Apdex totalTolerated float64 // Seconds, or count for Apdex exclusiveFailed float64 // Seconds, or count for Apdex min float64 // Seconds max float64 // Seconds sumSquares float64 // Seconds**2, or 0 for Apdex } func metricDataFromDuration(duration, exclusive time.Duration) metricData { ds := duration.Seconds() return metricData{ countSatisfied: 1, totalTolerated: ds, exclusiveFailed: exclusive.Seconds(), min: ds, max: ds, sumSquares: ds * ds, } } type metric struct { forced metricForce data metricData } type metricTable struct { metricPeriodStart time.Time failedHarvests int maxTableSize int // After this max is reached, only forced metrics are added metrics map[metricID]*metric } func newMetricTable(maxTableSize int, now time.Time) *metricTable { return &metricTable{ metricPeriodStart: now, metrics: make(map[metricID]*metric), maxTableSize: maxTableSize, failedHarvests: 0, } } func (mt *metricTable) full() bool { return len(mt.metrics) >= mt.maxTableSize } func (data *metricData) aggregate(src metricData) { data.countSatisfied += src.countSatisfied data.totalTolerated += src.totalTolerated data.exclusiveFailed += src.exclusiveFailed if src.min < data.min { data.min = src.min } if src.max > data.max { data.max = src.max } data.sumSquares += src.sumSquares } func (mt *metricTable) mergeMetric(id metricID, m metric) { if to := mt.metrics[id]; nil != to { to.data.aggregate(m.data) return } if mt.full() && (unforced == m.forced) { mt.addSingleCount(supportabilityDropped, forced) return } // NOTE: `new` is used in place of `&m` since the latter will make `m` // get heap allocated regardless of whether or not this line gets // reached (running go version go1.5 darwin/amd64). See // BenchmarkAddingSameMetrics. alloc := new(metric) *alloc = m mt.metrics[id] = alloc } func (mt *metricTable) mergeFailed(from *metricTable) { fails := from.failedHarvests + 1 if fails >= failedMetricAttemptsLimit { return } if from.metricPeriodStart.Before(mt.metricPeriodStart) { mt.metricPeriodStart = from.metricPeriodStart } mt.failedHarvests = fails mt.merge(from, "") } func (mt *metricTable) merge(from *metricTable, newScope string) { if newScope == "" { for id, m := range from.metrics { mt.mergeMetric(id, *m) } } else { for id, m := range from.metrics { mt.mergeMetric(metricID{Name: id.Name, Scope: newScope}, *m) } } } func (mt *metricTable) add(name, scope string, data metricData, force metricForce) { mt.mergeMetric(metricID{Name: name, Scope: scope}, metric{data: data, forced: force}) } func (mt *metricTable) addCount(name string, count float64, force metricForce) { mt.add(name, "", metricData{countSatisfied: count}, force) } func (mt *metricTable) addSingleCount(name string, force metricForce) { mt.addCount(name, float64(1), force) } func (mt *metricTable) addDuration(name, scope string, duration, exclusive time.Duration, force metricForce) { mt.add(name, scope, metricDataFromDuration(duration, exclusive), force) } func (mt *metricTable) addValueExclusive(name, scope string, total, exclusive float64, force metricForce) { data := metricData{ countSatisfied: 1, totalTolerated: total, exclusiveFailed: exclusive, min: total, max: total, sumSquares: total * total, } mt.add(name, scope, data, force) } func (mt *metricTable) addValue(name, scope string, total float64, force metricForce) { mt.addValueExclusive(name, scope, total, total, force) } func (mt *metricTable) addApdex(name, scope string, apdexThreshold time.Duration, zone apdexZone, force metricForce) { apdexSeconds := apdexThreshold.Seconds() data := metricData{min: apdexSeconds, max: apdexSeconds} switch zone { case apdexSatisfying: data.countSatisfied = 1 case apdexTolerating: data.totalTolerated = 1 case apdexFailing: data.exclusiveFailed = 1 } mt.add(name, scope, data, force) } func (mt *metricTable) CollectorJSON(agentRunID string, now time.Time) ([]byte, error) { if 0 == len(mt.metrics) { return nil, nil } estimatedBytesPerMetric := 128 estimatedLen := len(mt.metrics) * estimatedBytesPerMetric buf := bytes.NewBuffer(make([]byte, 0, estimatedLen)) buf.WriteByte('[') jsonx.AppendString(buf, agentRunID) buf.WriteByte(',') jsonx.AppendInt(buf, mt.metricPeriodStart.Unix()) buf.WriteByte(',') jsonx.AppendInt(buf, now.Unix()) buf.WriteByte(',') buf.WriteByte('[') first := true for id, metric := range mt.metrics { if first { first = false } else { buf.WriteByte(',') } buf.WriteByte('[') buf.WriteByte('{') buf.WriteString(`"name":`) jsonx.AppendString(buf, id.Name) if id.Scope != "" { buf.WriteString(`,"scope":`) jsonx.AppendString(buf, id.Scope) } buf.WriteByte('}') buf.WriteByte(',') jsonx.AppendFloatArray(buf, metric.data.countSatisfied, metric.data.totalTolerated, metric.data.exclusiveFailed, metric.data.min, metric.data.max, metric.data.sumSquares) buf.WriteByte(']') } buf.WriteByte(']') buf.WriteByte(']') return buf.Bytes(), nil } func (mt *metricTable) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return mt.CollectorJSON(agentRunID, harvestStart) } func (mt *metricTable) MergeIntoHarvest(h *harvest) { h.Metrics.mergeFailed(mt) } func (mt *metricTable) ApplyRules(rules internal.MetricRules) *metricTable { if nil == rules { return mt } if len(rules) == 0 { return mt } applied := newMetricTable(mt.maxTableSize, mt.metricPeriodStart) cache := make(map[string]string) for id, m := range mt.metrics { out, ok := cache[id.Name] if !ok { out = rules.Apply(id.Name) cache[id.Name] = out } if "" != out { applied.mergeMetric(metricID{Name: out, Scope: id.Scope}, *m) } } return applied } func (mt *metricTable) EndpointMethod() string { return cmdMetrics } go-agent-3.42.0/v3/newrelic/metrics_test.go000066400000000000000000000257211510742411500205040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) var ( start = time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) end = time.Date(2014, time.November, 28, 1, 2, 0, 0, time.UTC) ) func TestEmptyMetrics(t *testing.T) { mt := newMetricTable(20, start) js, err := mt.CollectorJSON(`12345`, end) if nil != err { t.Fatal(err) } if nil != js { t.Error(string(js)) } } func isValidJSON(data []byte) error { var v interface{} return json.Unmarshal(data, &v) } func TestMetrics(t *testing.T) { mt := newMetricTable(20, start) mt.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("two", "my_scope", 4*time.Second, 2*time.Second, unforced) mt.addDuration("one", "my_scope", 2*time.Second, 1*time.Second, unforced) mt.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) mt.addApdex("apdex satisfied", "", 9*time.Second, apdexSatisfying, unforced) mt.addApdex("apdex satisfied", "", 8*time.Second, apdexSatisfying, unforced) mt.addApdex("apdex tolerated", "", 7*time.Second, apdexTolerating, unforced) mt.addApdex("apdex tolerated", "", 8*time.Second, apdexTolerating, unforced) mt.addApdex("apdex failed", "my_scope", 1*time.Second, apdexFailing, unforced) mt.addCount("count 123", float64(123), unforced) mt.addSingleCount("count 1", unforced) expectMetrics(t, mt, []internal.WantMetric{ {Name: "apdex satisfied", Scope: "", Forced: false, Data: []float64{2, 0, 0, 8, 9, 0}}, {Name: "apdex tolerated", Scope: "", Forced: false, Data: []float64{0, 2, 0, 7, 8, 0}}, {Name: "one", Scope: "", Forced: false, Data: []float64{2, 4, 2, 2, 2, 8}}, {Name: "apdex failed", Scope: "my_scope", Forced: false, Data: []float64{0, 0, 1, 1, 1, 0}}, {Name: "one", Scope: "my_scope", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "my_scope", Forced: false, Data: []float64{1, 4, 2, 4, 4, 16}}, {Name: "count 123", Scope: "", Forced: false, Data: []float64{123, 0, 0, 0, 0, 0}}, {Name: "count 1", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) js, err := mt.Data("12345", end) if nil != err { t.Error(err) } // The JSON metric order is not deterministic, so we merely test that it // is valid JSON. if err := isValidJSON(js); nil != err { t.Error(err, string(js)) } } func TestApplyRules(t *testing.T) { js := `[ { "ignore":false, "each_segment":false, "terminate_chain":true, "replacement":"been_renamed", "replace_all":false, "match_expression":"one$", "eval_order":1 }, { "ignore":true, "each_segment":false, "terminate_chain":true, "replace_all":false, "match_expression":"ignore_me", "eval_order":1 }, { "ignore":false, "each_segment":false, "terminate_chain":true, "replacement":"merge_me", "replace_all":false, "match_expression":"merge_me[0-9]+$", "eval_order":1 } ]` var rules internal.MetricRules err := json.Unmarshal([]byte(js), &rules) if nil != err { t.Fatal(err) } mt := newMetricTable(20, start) mt.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("one", "scope1", 2*time.Second, 1*time.Second, unforced) mt.addDuration("one", "scope2", 2*time.Second, 1*time.Second, unforced) mt.addDuration("ignore_me", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("ignore_me", "scope1", 2*time.Second, 1*time.Second, unforced) mt.addDuration("ignore_me", "scope2", 2*time.Second, 1*time.Second, unforced) mt.addDuration("merge_me1", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("merge_me2", "", 2*time.Second, 1*time.Second, unforced) applied := mt.ApplyRules(rules) expectMetrics(t, applied, []internal.WantMetric{ {Name: "been_renamed", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "been_renamed", Scope: "scope1", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "been_renamed", Scope: "scope2", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "merge_me", Scope: "", Forced: false, Data: []float64{2, 4, 2, 2, 2, 8}}, }) } func TestApplyEmptyRules(t *testing.T) { js := `[]` var rules internal.MetricRules err := json.Unmarshal([]byte(js), &rules) if nil != err { t.Fatal(err) } mt := newMetricTable(20, start) mt.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("one", "my_scope", 2*time.Second, 1*time.Second, unforced) applied := mt.ApplyRules(rules) expectMetrics(t, applied, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "one", Scope: "my_scope", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestApplyNilRules(t *testing.T) { var rules internal.MetricRules mt := newMetricTable(20, start) mt.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) mt.addDuration("one", "my_scope", 2*time.Second, 1*time.Second, unforced) applied := mt.ApplyRules(rules) expectMetrics(t, applied, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "one", Scope: "my_scope", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestForced(t *testing.T) { mt := newMetricTable(0, start) mt.addDuration("unforced", "", 1*time.Second, 1*time.Second, unforced) mt.addDuration("forced", "", 2*time.Second, 2*time.Second, forced) expectMetrics(t, mt, []internal.WantMetric{ {Name: "forced", Scope: "", Forced: true, Data: []float64{1, 2, 2, 2, 2, 4}}, {Name: supportabilityDropped, Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestMetricsMergeIntoEmpty(t *testing.T) { src := newMetricTable(20, start) src.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) dest := newMetricTable(20, start) dest.merge(src, "") expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestMetricsMergeFromEmpty(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, start) dest.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) dest.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) dest.merge(src, "") expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestMetricsMerge(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, start) dest.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) dest.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("three", "", 2*time.Second, 1*time.Second, unforced) dest.merge(src, "") expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "", Forced: false, Data: []float64{2, 4, 2, 2, 2, 8}}, {Name: "three", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestMergeFailedSuccess(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, end) dest.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) dest.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("three", "", 2*time.Second, 1*time.Second, unforced) if 0 != dest.failedHarvests { t.Fatal(dest.failedHarvests) } dest.mergeFailed(src) expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "", Forced: false, Data: []float64{2, 4, 2, 2, 2, 8}}, {Name: "three", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func TestMergeFailedLimitReached(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, end) dest.addDuration("one", "", 2*time.Second, 1*time.Second, unforced) dest.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) src.addDuration("three", "", 2*time.Second, 1*time.Second, unforced) src.failedHarvests = failedMetricAttemptsLimit dest.mergeFailed(src) expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, {Name: "two", Scope: "", Forced: false, Data: []float64{1, 2, 1, 2, 2, 4}}, }) } func BenchmarkMetricTableCollectorJSON(b *testing.B) { mt := newMetricTable(2000, time.Now()) md := metricData{ countSatisfied: 1234567812345678.1234567812345678, totalTolerated: 1234567812345678.1234567812345678, exclusiveFailed: 1234567812345678.1234567812345678, min: 1234567812345678.1234567812345678, max: 1234567812345678.1234567812345678, sumSquares: 1234567812345678.1234567812345678, } for i := 0; i < 20; i++ { scope := fmt.Sprintf("WebTransaction/Uri/myblog2/%d", i) for j := 0; j < 20; j++ { name := fmt.Sprintf("Datastore/statement/MySQL/City%d/insert", j) mt.add(name, "", md, forced) mt.add(name, scope, md, forced) name = fmt.Sprintf("WebTransaction/Uri/myblog2/newPost_rum_%d.php", j) mt.add(name, "", md, forced) mt.add(name, scope, md, forced) } } data, err := mt.CollectorJSON("12345", time.Now()) if nil != err { b.Fatal(err) } if err := isValidJSON(data); nil != err { b.Fatal(err, string(data)) } b.ResetTimer() b.ReportAllocs() id := "12345" now := time.Now() for i := 0; i < b.N; i++ { mt.CollectorJSON(id, now) } } func BenchmarkAddingSameMetrics(b *testing.B) { name := "my_name" scope := "my_scope" duration := 2 * time.Second exclusive := 1 * time.Second mt := newMetricTable(2000, time.Now()) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { mt.addDuration(name, scope, duration, exclusive, forced) mt.addSingleCount(name, forced) } } func TestMergedMetricsAreCopied(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, start) src.addSingleCount("zip", unforced) dest.merge(src, "") src.addSingleCount("zip", unforced) expectMetrics(t, dest, []internal.WantMetric{ {Name: "zip", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } func TestMergedWithScope(t *testing.T) { src := newMetricTable(20, start) dest := newMetricTable(20, start) src.addSingleCount("one", unforced) src.addDuration("two", "", 2*time.Second, 1*time.Second, unforced) dest.addDuration("two", "my_scope", 2*time.Second, 1*time.Second, unforced) dest.merge(src, "my_scope") expectMetrics(t, dest, []internal.WantMetric{ {Name: "one", Scope: "my_scope", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "two", Scope: "my_scope", Forced: false, Data: []float64{2, 4, 2, 2, 2, 8}}, }) } go-agent-3.42.0/v3/newrelic/obfuscate.go000066400000000000000000000015711510742411500177470ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/base64" "errors" ) // deobfuscate deobfuscates a byte array. func deobfuscate(in string, key []byte) ([]byte, error) { if len(key) == 0 { return nil, errors.New("key cannot be zero length") } decoded, err := base64.StdEncoding.DecodeString(in) if err != nil { return nil, err } out := make([]byte, len(decoded)) for i, c := range decoded { out[i] = c ^ key[i%len(key)] } return out, nil } // obfuscate obfuscates a byte array for transmission in CAT and RUM. func obfuscate(in, key []byte) (string, error) { if len(key) == 0 { return "", errors.New("key cannot be zero length") } out := make([]byte, len(in)) for i, c := range in { out[i] = c ^ key[i%len(key)] } return base64.StdEncoding.EncodeToString(out), nil } go-agent-3.42.0/v3/newrelic/obfuscate_test.go000066400000000000000000000034411510742411500210040ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" ) func TestDeobfuscate(t *testing.T) { var out []byte var err error for _, in := range []string{"", "foo"} { out, err = deobfuscate(in, []byte("")) if err == nil { t.Error("error is nil for an empty key") } if out != nil { t.Errorf("out is not nil; got: %s", out) } } for _, in := range []string{"invalid_base64", "=moreinvalidbase64", "xx"} { out, err = deobfuscate(in, []byte("")) if err == nil { t.Error("error is nil for invalid base64") } if out != nil { t.Errorf("out is not nil; got: %s", out) } } for _, test := range []struct { input string key string expected string }{ {"", "BLAHHHH", ""}, {"NikyPBs8OisiJg==", "BLAHHHH", "testString"}, } { out, err = deobfuscate(test.input, []byte(test.key)) if err != nil { t.Errorf("error expected to be nil; got: %v", err) } if string(out) != test.expected { t.Errorf("output mismatch; expected: %s; got: %s", test.expected, out) } } } func TestObfuscate(t *testing.T) { var out string var err error for _, in := range []string{"", "foo"} { out, err = obfuscate([]byte(in), []byte("")) if err == nil { t.Error("error is nil for an empty key") } if out != "" { t.Errorf("out is not an empty string; got: %s", out) } } for _, test := range []struct { input string key string expected string }{ {"", "BLAHHHH", ""}, {"testString", "BLAHHHH", "NikyPBs8OisiJg=="}, } { out, err = obfuscate([]byte(test.input), []byte(test.key)) if err != nil { t.Errorf("error expected to be nil; got: %v", err) } if out != test.expected { t.Errorf("output mismatch; expected: %s; got: %s", test.expected, out) } } } go-agent-3.42.0/v3/newrelic/oom_monitor.go000066400000000000000000000126741510742411500203430ustar00rootroot00000000000000// Copyright 2022 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "runtime" "sync" "time" ) type heapHighWaterMarkAlarmSet struct { lock sync.RWMutex // protects creation of the ticker and access to map sampleTicker *time.Ticker // once made, only read by monitor goroutine alarms map[uint64]func(uint64, *runtime.MemStats) done chan byte } // This is a gross, high-level whole-heap memory monitor which can be used to monitor, track, // and trigger an application's response to running out of memory as an initial step or when // more expensive or sophisticated analysis such as per-routine memory usage tracking is not // needed. // // For this, we simply configure one or more heap memory limits and for each, register a callback // function to be called any time we notice that the total heap allocation reaches or exceeds that // limit. Note that this means if the allocation size crosses multiple limits, then multiple // callbacks will be triggered since each of their criteria will be met. // // HeapHighWaterMarkAlarmEnable starts the periodic sampling of the runtime heap allocation // of the application, at the user-provided sampling interval. Calling HeapHighWaterMarkAlarmEnable // with an interval less than or equal to 0 is equivalent to calling HeapHighWaterMarkAlarmDisable. // // If there was already a running heap monitor, this merely changes its sample interval time. func (a *Application) HeapHighWaterMarkAlarmEnable(interval time.Duration) { if a == nil || a.app == nil { return } if interval <= 0 { a.HeapHighWaterMarkAlarmDisable() return } a.app.heapHighWaterMarkAlarms.lock.Lock() defer a.app.heapHighWaterMarkAlarms.lock.Unlock() if a.app.heapHighWaterMarkAlarms.sampleTicker == nil { a.app.heapHighWaterMarkAlarms.sampleTicker = time.NewTicker(interval) a.app.heapHighWaterMarkAlarms.done = make(chan byte) go a.app.heapHighWaterMarkAlarms.monitor() } else { a.app.heapHighWaterMarkAlarms.sampleTicker.Reset(interval) } } func (as *heapHighWaterMarkAlarmSet) monitor() { for { select { case <-as.sampleTicker.C: var m runtime.MemStats runtime.ReadMemStats(&m) as.lock.RLock() if as.alarms != nil { for limit, callback := range as.alarms { if m.HeapAlloc >= limit { callback(limit, &m) } } } as.lock.RUnlock() case <-as.done: return } } } // HeapHighWaterMarkAlarmShutdown stops the monitoring goroutine and deallocates the entire // monitoring completely. All alarms are canceled and disabled. func (a *Application) HeapHighWaterMarkAlarmShutdown() { if a == nil || a.app == nil { return } a.app.heapHighWaterMarkAlarms.lock.Lock() defer a.app.heapHighWaterMarkAlarms.lock.Unlock() if a.app.heapHighWaterMarkAlarms.sampleTicker != nil { a.app.heapHighWaterMarkAlarms.sampleTicker.Stop() } if a.app.heapHighWaterMarkAlarms.done != nil { a.app.heapHighWaterMarkAlarms.done <- 0 } if a.app.heapHighWaterMarkAlarms.alarms != nil { clear(a.app.heapHighWaterMarkAlarms.alarms) a.app.heapHighWaterMarkAlarms.alarms = nil } a.app.heapHighWaterMarkAlarms.sampleTicker = nil } // HeapHighWaterMarkAlarmDisable stops sampling the heap memory allocation started by // HeapHighWaterMarkAlarmEnable. It is safe to call even if HeapHighWaterMarkAlarmEnable was // never called or the alarms were already disabled. func (a *Application) HeapHighWaterMarkAlarmDisable() { if a == nil || a.app == nil { return } a.app.heapHighWaterMarkAlarms.lock.Lock() defer a.app.heapHighWaterMarkAlarms.lock.Unlock() if a.app.heapHighWaterMarkAlarms.sampleTicker != nil { a.app.heapHighWaterMarkAlarms.sampleTicker.Stop() } } // HeapHighWaterMarkAlarmSet adds a heap memory high water mark alarm to the set of alarms // being tracked by the running heap monitor. Memory is checked on the interval specified to // the last call to HeapHighWaterMarkAlarmEnable, and if at that point the globally allocated heap // memory is at least the specified size, the provided callback function will be invoked. This // method may be called multiple times to register any number of callback functions to respond // to different memory thresholds. For example, you may wish to make measurements or warnings // of various urgency levels before finally taking action. // // If HeapHighWaterMarkAlarmSet is called with the same memory limit as a previous call, the // supplied callback function will replace the one previously registered for that limit. If // the function is given as nil, then that memory limit alarm is removed from the list. func (a *Application) HeapHighWaterMarkAlarmSet(limit uint64, f func(uint64, *runtime.MemStats)) { if a == nil || a.app == nil { return } a.app.heapHighWaterMarkAlarms.lock.Lock() defer a.app.heapHighWaterMarkAlarms.lock.Unlock() if a.app.heapHighWaterMarkAlarms.alarms == nil { a.app.heapHighWaterMarkAlarms.alarms = make(map[uint64]func(uint64, *runtime.MemStats)) } if f == nil { delete(a.app.heapHighWaterMarkAlarms.alarms, limit) } else { a.app.heapHighWaterMarkAlarms.alarms[limit] = f } } // HeapHighWaterMarkAlarmClearAll removes all high water mark alarms from the memory monitor // set. func (a *Application) HeapHighWaterMarkAlarmClearAll() { if a == nil || a.app == nil { return } a.app.heapHighWaterMarkAlarms.lock.Lock() defer a.app.heapHighWaterMarkAlarms.lock.Unlock() if a.app.heapHighWaterMarkAlarms.alarms == nil { return } clear(a.app.heapHighWaterMarkAlarms.alarms) } go-agent-3.42.0/v3/newrelic/oom_monitor_test.go000066400000000000000000000215321510742411500213730ustar00rootroot00000000000000package newrelic import ( "runtime" "testing" "time" ) func TestHeapHighWaterMarkAlarmNilAppSafety(t *testing.T) { tests := []struct { name string fn func(*Application) }{ {"Enable", func(app *Application) { app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) }}, {"Disable", func(app *Application) { app.HeapHighWaterMarkAlarmDisable() }}, {"Shutdown", func(app *Application) { app.HeapHighWaterMarkAlarmShutdown() }}, {"Set", func(app *Application) { app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) }}, {"ClearAll", func(app *Application) { app.HeapHighWaterMarkAlarmClearAll() }}, } appCases := []struct { name string app *Application }{ {"nil app", nil}, {"nil app.app", &Application{app: nil}}, } for _, appCase := range appCases { for _, tt := range tests { t.Run(appCase.name+"_"+tt.name, func(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("%s panicked on %s: %v", tt.name, appCase.name, r) } }() tt.fn(appCase.app) }) } } } func TestHeapHighWaterMarkAlarmEnable(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.app.heapHighWaterMarkAlarms.lock.RLock() ticker := app.app.heapHighWaterMarkAlarms.sampleTicker done := app.app.heapHighWaterMarkAlarms.done app.app.heapHighWaterMarkAlarms.lock.RUnlock() if ticker == nil || done == nil { t.Error("Expected ticker and done channel to be created") } } func TestHeapHighWaterMarkAlarmEnableResetTicker(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) // Enable with initial interval app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.app.heapHighWaterMarkAlarms.lock.RLock() oldTicker := app.app.heapHighWaterMarkAlarms.sampleTicker app.app.heapHighWaterMarkAlarms.lock.RUnlock() if oldTicker == nil { t.Fatal("Expected ticker to be created on first enable") } // Enable with new interval - should reset existing ticker app.HeapHighWaterMarkAlarmEnable(400 * time.Millisecond) app.app.heapHighWaterMarkAlarms.lock.RLock() newTicker := app.app.heapHighWaterMarkAlarms.sampleTicker app.app.heapHighWaterMarkAlarms.lock.RUnlock() if newTicker == nil { t.Fatal("Expected ticker to exist after reset") } if oldTicker != newTicker { t.Error("Expected ticker to be the same instance after reset") } // Verify ticker is functional and doesn't tick too frequently tickCount := 0 timeout := time.After(1200 * time.Millisecond) // Give enough time for 2+ ticks for { select { case <-newTicker.C: tickCount++ if tickCount == 2 { // Stop after getting expected ticks return } else if tickCount > 2 { t.Errorf("Ticker ticked more than expected: %d times", tickCount) return } case <-timeout: if tickCount == 0 { t.Error("Ticker should have ticked at least once within timeout") } return } } } func TestHeapHighWaterMarkAlarmCallbackTriggered(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(10 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() callbackCh := make(chan struct{}, 1) var calledLimit uint64 var calledMemStats *runtime.MemStats app.HeapHighWaterMarkAlarmSet(1, func(limit uint64, memStats *runtime.MemStats) { calledLimit = limit calledMemStats = memStats select { case callbackCh <- struct{}{}: default: } }) select { case <-callbackCh: // Callback triggered case <-time.After(200 * time.Millisecond): t.Fatal("Timeout: callback was not called when heap allocation exceeds limit") } if calledLimit != 1 { t.Errorf("Expected callback to be called with limit 1, got %d", calledLimit) } if calledMemStats == nil { t.Error("Expected callback to be called with valid MemStats") } if calledMemStats != nil && calledMemStats.HeapAlloc < 1 { t.Errorf("Expected HeapAlloc to be >= 1, got %d", calledMemStats.HeapAlloc) } } func TestHeapHighWaterMarkWithInvalidInterval(t *testing.T) { tests := []struct { name string interval time.Duration }{ {"zero interval", 0}, {"negative interval", -1 * time.Second}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(tt.interval) defer app.HeapHighWaterMarkAlarmShutdown() app.app.heapHighWaterMarkAlarms.lock.RLock() ticker := app.app.heapHighWaterMarkAlarms.sampleTicker app.app.heapHighWaterMarkAlarms.lock.RUnlock() if ticker != nil { t.Error("Expected ticker to be nil with invalid interval") } }) } } func TestHeapHighWaterMarkAlarmDisable(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.HeapHighWaterMarkAlarmDisable() app.app.heapHighWaterMarkAlarms.lock.RLock() ticker := app.app.heapHighWaterMarkAlarms.sampleTicker app.app.heapHighWaterMarkAlarms.lock.RUnlock() if ticker == nil { t.Error("Expected ticker to still exist after disable") } } func TestHeapHighWaterMarkAlarmShutdown(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) app.HeapHighWaterMarkAlarmShutdown() app.app.heapHighWaterMarkAlarms.lock.RLock() ticker := app.app.heapHighWaterMarkAlarms.sampleTicker alarms := app.app.heapHighWaterMarkAlarms.alarms app.app.heapHighWaterMarkAlarms.lock.RUnlock() if ticker != nil || alarms != nil { t.Error("Expected ticker and alarms to be nil after shutdown") } } func TestHeapHighWaterMarkAlarmSet(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) if app.app == nil { t.Error("app.app is nil") return } app.app.heapHighWaterMarkAlarms.lock.RLock() alarms := app.app.heapHighWaterMarkAlarms.alarms app.app.heapHighWaterMarkAlarms.lock.RUnlock() if alarms == nil || len(alarms) != 1 || alarms[1024] == nil { t.Error("Expected one alarm with a valid callback") } } func TestHeapHighWaterMarkAlarmSetReplaceCallback(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) app.app.heapHighWaterMarkAlarms.lock.RLock() alarms := app.app.heapHighWaterMarkAlarms.alarms app.app.heapHighWaterMarkAlarms.lock.RUnlock() if len(alarms) != 1 { t.Error("Expected one alarm after replacing callback") } } func TestHeapHighWaterMarkAlarmSetRemoveWithNil(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) app.HeapHighWaterMarkAlarmSet(1024, nil) app.app.heapHighWaterMarkAlarms.lock.RLock() alarms := app.app.heapHighWaterMarkAlarms.alarms app.app.heapHighWaterMarkAlarms.lock.RUnlock() if len(alarms) != 0 { t.Error("Expected no alarms after removing with nil") } } func TestHeapHighWaterMarkAlarmClearAll(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) app.HeapHighWaterMarkAlarmSet(2048, func(uint64, *runtime.MemStats) {}) app.HeapHighWaterMarkAlarmClearAll() app.app.heapHighWaterMarkAlarms.lock.RLock() alarms := app.app.heapHighWaterMarkAlarms.alarms app.app.heapHighWaterMarkAlarms.lock.RUnlock() if len(alarms) != 0 { t.Error("Expected no alarms after clearing all") } } func TestHeapHighWaterMarkAlarmClearAllWithNilAlarms(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) defer app.HeapHighWaterMarkAlarmShutdown() // Ensure alarms is nil app.app.heapHighWaterMarkAlarms.lock.Lock() app.app.heapHighWaterMarkAlarms.alarms = nil app.app.heapHighWaterMarkAlarms.lock.Unlock() // Should not panic or error app.HeapHighWaterMarkAlarmClearAll() } go-agent-3.42.0/v3/newrelic/priority.go000066400000000000000000000025631510742411500176570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "fmt" "strings" ) // priority allows for a priority sampling of events. When an event // is created it is given a priority. Whenever an event pool is // full and events need to be dropped, the events with the lowest priority // are dropped. type priority float32 // According to spec, Agents SHOULD truncate the value to at most 6 // digits past the decimal point. const ( priorityFormat = "%.6f" ) func newPriorityFromRandom(rnd func() float32) priority { for { if r := rnd(); 0.0 != r { return priority(r) } } } // newPriority returns a new priority. func newPriority() priority { return newPriorityFromRandom(randFloat32) } // Float32 returns the priority as a float32. func (p priority) Float32() float32 { return float32(p) } func (p priority) isLowerPriority(y priority) bool { return p < y } // MarshalJSON limits the number of decimals. func (p priority) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(priorityFormat, p)), nil } // WriteJSON limits the number of decimals. func (p priority) WriteJSON(buf *bytes.Buffer) { fmt.Fprintf(buf, priorityFormat, p) } func (p priority) traceStateFormat() string { s := fmt.Sprintf(priorityFormat, p) s = strings.TrimRight(s, "0") return strings.TrimRight(s, ".") } go-agent-3.42.0/v3/newrelic/priority_test.go000066400000000000000000000017101510742411500207070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" ) func TestIsLowerPriority(t *testing.T) { low := priority(0.0) middle := priority(0.1) high := priority(0.999999) if !low.isLowerPriority(middle) { t.Error(low, middle) } if high.isLowerPriority(middle) { t.Error(high, middle) } if high.isLowerPriority(high) { t.Error(high, high) } } func TestTraceStateFormat(t *testing.T) { testcases := []struct { input float64 expected string }{ {input: 0, expected: "0"}, {input: 0.1, expected: "0.1"}, {input: 0.7654321, expected: "0.765432"}, {input: 10.7654321, expected: "10.765432"}, {input: 0.99999999999, expected: "1"}, } for _, tc := range testcases { p := priority(tc.input) if out := p.traceStateFormat(); out != tc.expected { t.Errorf("wrong priority format for %f: expected=%s actual=%s", tc.input, tc.expected, out) } } } go-agent-3.42.0/v3/newrelic/queuing.go000066400000000000000000000030121510742411500174410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "strconv" "strings" "time" ) const ( xRequestStart = "X-Request-Start" xQueueStart = "X-Queue-Start" ) var ( earliestAcceptableSeconds = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() latestAcceptableSeconds = time.Date(2050, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() ) func checkQueueTimeSeconds(secondsFloat float64) time.Time { seconds := int64(secondsFloat) nanos := int64((secondsFloat - float64(seconds)) * (1000.0 * 1000.0 * 1000.0)) if seconds > earliestAcceptableSeconds && seconds < latestAcceptableSeconds { return time.Unix(seconds, nanos) } return time.Time{} } func parseQueueTime(s string) time.Time { f, err := strconv.ParseFloat(s, 64) if nil != err { return time.Time{} } if f <= 0 { return time.Time{} } // try microseconds if t := checkQueueTimeSeconds(f / (1000.0 * 1000.0)); !t.IsZero() { return t } // try milliseconds if t := checkQueueTimeSeconds(f / (1000.0)); !t.IsZero() { return t } // try seconds if t := checkQueueTimeSeconds(f); !t.IsZero() { return t } return time.Time{} } func queueDuration(hdr http.Header, txnStart time.Time) time.Duration { s := hdr.Get(xQueueStart) if "" == s { s = hdr.Get(xRequestStart) } if "" == s { return 0 } s = strings.TrimPrefix(s, "t=") qt := parseQueueTime(s) if qt.IsZero() { return 0 } if qt.After(txnStart) { return 0 } return txnStart.Sub(qt) } go-agent-3.42.0/v3/newrelic/queuing_test.go000066400000000000000000000036371510742411500205150ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" "time" ) func TestParseQueueTime(t *testing.T) { badInput := []string{ "", "nope", "t", "0", "0.0", "9999999999999999999999999999999999999999999999999", "-1368811467146000", "3000000000", "3000000000000", "900000000", "900000000000", } for _, s := range badInput { if qt := parseQueueTime(s); !qt.IsZero() { t.Error(s, qt) } } testcases := []struct { input string expect int64 }{ // Microseconds {"1368811467146000", 1368811467}, // Milliseconds {"1368811467146.000", 1368811467}, {"1368811467146", 1368811467}, // Seconds {"1368811467.146000", 1368811467}, {"1368811467.146", 1368811467}, {"1368811467", 1368811467}, } for _, tc := range testcases { qt := parseQueueTime(tc.input) if qt.Unix() != tc.expect { t.Error(tc.input, tc.expect, qt, qt.UnixNano()) } } } func TestQueueDuration(t *testing.T) { hdr := make(http.Header) hdr.Set("X-Queue-Start", "1465798814") qd := queueDuration(hdr, time.Unix(1465798816, 0)) if qd != 2*time.Second { t.Error(qd) } hdr = make(http.Header) hdr.Set("X-Request-Start", "1465798814") qd = queueDuration(hdr, time.Unix(1465798816, 0)) if qd != 2*time.Second { t.Error(qd) } hdr = make(http.Header) qd = queueDuration(hdr, time.Unix(1465798816, 0)) if qd != 0 { t.Error(qd) } hdr = make(http.Header) hdr.Set("X-Request-Start", "invalid-time") qd = queueDuration(hdr, time.Unix(1465798816, 0)) if qd != 0 { t.Error(qd) } hdr = make(http.Header) hdr.Set("X-Queue-Start", "t=1465798814") qd = queueDuration(hdr, time.Unix(1465798816, 0)) if qd != 2*time.Second { t.Error(qd) } // incorrect time order hdr = make(http.Header) hdr.Set("X-Queue-Start", "t=1465798816") qd = queueDuration(hdr, time.Unix(1465798814, 0)) if qd != 0 { t.Error(qd) } } go-agent-3.42.0/v3/newrelic/rand.go000066400000000000000000000025421510742411500167170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "math/rand" "sync" "time" ) var ( seededRand = struct { sync.Mutex *rand.Rand }{ Rand: rand.New(rand.NewSource(int64(time.Now().UnixNano()))), } ) // randUint64 returns a random uint64. // // IMPORTANT! The default rand package functions are not used, since we want to // minimize the chance that different Go processes duplicate the same // transaction id. (Note that the rand top level functions "use a default // shared Source that produces a deterministic sequence of values each time a // program is run" (and we don't seed the shared Source to avoid changing // customer apps' behavior)). func randUint64() uint64 { seededRand.Lock() defer seededRand.Unlock() u1 := seededRand.Uint32() u2 := seededRand.Uint32() return (uint64(u1) << 32) | uint64(u2) } // randUint32 returns a random uint32. func randUint32() uint32 { seededRand.Lock() defer seededRand.Unlock() return seededRand.Uint32() } // randFloat32 returns a random float32 in [0.0,1.0). func randFloat32() float32 { seededRand.Lock() defer seededRand.Unlock() return seededRand.Float32() } // randUint64N returns a random int64 that's // between 0 and the passed in max, non-inclusive func randUint64N(max uint64) uint64 { return randUint64() % max } go-agent-3.42.0/v3/newrelic/reservoir_limits_test.go000066400000000000000000000073651510742411500224430ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "testing" "github.com/newrelic/go-agent/v3/internal" ) // Check Default Value func TestCustomLimitsBasic(t *testing.T) { limit := internal.MaxCustomEvents limits := &internal.RequestEventLimits{ CustomEvents: limit, } // This function will mock a connect reply from the server mockReplyFunction := func(reply *internal.ConnectReply) { reply.MockConnectReplyEventLimits(limits) } testApp := newTestApp( mockReplyFunction, ConfigCustomInsightsEventsMaxSamplesStored(limit), ) customEventRate := limit / (60 / internal.CustomEventHarvestsPerMinute) // Check if custom event queue capacity == rate if customEventRate != testApp.app.testHarvest.CustomEvents.capacity() { t.Errorf("Custom Events Rate is not equal to harvest: expected %d, actual %d", customEventRate, testApp.app.testHarvest.CustomEvents.capacity()) } } func TestCustomEventLimitUserSet(t *testing.T) { limit := 7000 limits := &internal.RequestEventLimits{ CustomEvents: limit, } mockReplyFunction := func(reply *internal.ConnectReply) { reply.MockConnectReplyEventLimits(limits) } testApp := newTestApp( mockReplyFunction, ConfigCustomInsightsEventsMaxSamplesStored(limit), ) customEventRate := limit / (60 / internal.CustomEventHarvestsPerMinute) if customEventRate != testApp.app.testHarvest.CustomEvents.capacity() { t.Errorf("Custom Events Rate is not equal to harvest: expected %d, actual %d", customEventRate, testApp.app.testHarvest.CustomEvents.capacity()) } } func TestCustomLimitEnthusiast(t *testing.T) { limit := 100000 limits := &internal.RequestEventLimits{ CustomEvents: limit, } // This function will mock a connect reply from the server mockReplyFunction := func(reply *internal.ConnectReply) { reply.MockConnectReplyEventLimits(limits) } testApp := newTestApp( mockReplyFunction, ConfigCustomInsightsEventsMaxSamplesStored(limit), ) customEventRate := limit / (60 / internal.CustomEventHarvestsPerMinute) // Check if custom event queue capacity == rate if customEventRate != testApp.app.testHarvest.CustomEvents.capacity() { t.Errorf("Custom Events Rate is not equal to harvest: expected %d, actual %d", customEventRate, testApp.app.testHarvest.CustomEvents.capacity()) } } func TestCustomLimitsTypo(t *testing.T) { limit := 1000000 limits := &internal.RequestEventLimits{ CustomEvents: limit, } // This function will mock a connect reply from the server mockReplyFunction := func(reply *internal.ConnectReply) { reply.MockConnectReplyEventLimits(limits) } testApp := newTestApp( mockReplyFunction, ConfigCustomInsightsEventsMaxSamplesStored(limit), ) customEventRate := 100000 / (60 / internal.CustomEventHarvestsPerMinute) // Check if custom event queue capacity == rate if customEventRate != testApp.app.testHarvest.CustomEvents.capacity() { t.Errorf("Custom Events Rate is not equal to harvest: expected %d, actual %d", 8333, testApp.app.testHarvest.CustomEvents.capacity()) } } func TestCustomLimitZero(t *testing.T) { limit := 0 limits := &internal.RequestEventLimits{ CustomEvents: limit, } // This function will mock a connect reply from the server mockReplyFunction := func(reply *internal.ConnectReply) { reply.MockConnectReplyEventLimits(limits) } testApp := newTestApp( mockReplyFunction, ConfigCustomInsightsEventsMaxSamplesStored(limit), ) customEventRate := limit / (60 / internal.CustomEventHarvestsPerMinute) // Check if custom event queue capacity == rate if customEventRate != testApp.app.testHarvest.CustomEvents.capacity() { t.Errorf("Custom Events Rate is not equal to harvest: expected %d, actual %d", customEventRate, testApp.app.testHarvest.CustomEvents.capacity()) } } go-agent-3.42.0/v3/newrelic/rules_cache.go000066400000000000000000000021301510742411500202410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "sync" // rulesCache is designed to avoid applying url-rules, txn-name-rules, and // segment-rules since regexes are expensive! type rulesCache struct { sync.RWMutex cache map[rulesCacheKey]string maxCacheSize int } type rulesCacheKey struct { isWeb bool inputName string } func newRulesCache(maxCacheSize int) *rulesCache { return &rulesCache{ cache: make(map[rulesCacheKey]string, maxCacheSize), maxCacheSize: maxCacheSize, } } func (cache *rulesCache) find(inputName string, isWeb bool) string { if nil == cache { return "" } cache.RLock() defer cache.RUnlock() return cache.cache[rulesCacheKey{ inputName: inputName, isWeb: isWeb, }] } func (cache *rulesCache) set(inputName string, isWeb bool, finalName string) { if nil == cache { return } cache.Lock() defer cache.Unlock() if len(cache.cache) >= cache.maxCacheSize { return } cache.cache[rulesCacheKey{ inputName: inputName, isWeb: isWeb, }] = finalName } go-agent-3.42.0/v3/newrelic/rules_cache_test.go000066400000000000000000000034061510742411500213070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "testing" func TestRulesCache(t *testing.T) { testcases := []struct { input string isWeb bool output string }{ {input: "name1", isWeb: true, output: "WebTransaction/Go/name1"}, {input: "name1", isWeb: false, output: "OtherTransaction/Go/name1"}, {input: "name2", isWeb: true, output: "WebTransaction/Go/name2"}, {input: "name3", isWeb: true, output: "WebTransaction/Go/name3"}, {input: "zap/123/zip", isWeb: false, output: "OtherTransaction/Go/zap/*/zip"}, {input: "zap/45/zip", isWeb: false, output: "OtherTransaction/Go/zap/*/zip"}, } cache := newRulesCache(len(testcases)) for _, tc := range testcases { // Test that nothing is in the cache before population. if out := cache.find(tc.input, tc.isWeb); out != "" { t.Error(out, tc.input, tc.isWeb) } } for _, tc := range testcases { cache.set(tc.input, tc.isWeb, tc.output) } for _, tc := range testcases { // Test that everything is now in the cache as expected. if out := cache.find(tc.input, tc.isWeb); out != tc.output { t.Error(out, tc.input, tc.isWeb, tc.output) } } } func TestRulesCacheLimit(t *testing.T) { cache := newRulesCache(1) cache.set("name1", true, "WebTransaction/Go/name1") cache.set("name1", false, "OtherTransaction/Go/name1") if out := cache.find("name1", true); out != "WebTransaction/Go/name1" { t.Error(out) } if out := cache.find("name1", false); out != "" { t.Error(out) } } func TestRulesCacheNil(t *testing.T) { var cache *rulesCache // No panics should happen if the rules cache pointer is nil. if out := cache.find("name1", true); "" != out { t.Error(out) } cache.set("name1", false, "OtherTransaction/Go/name1") } go-agent-3.42.0/v3/newrelic/sampler.go000066400000000000000000000111551510742411500174360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "runtime" "time" "github.com/newrelic/go-agent/v3/internal/sysinfo" ) // systemSample is a system/runtime snapshot. type systemSample struct { when time.Time memStats runtime.MemStats usage sysinfo.Usage numGoroutine int numCPU int } func bytesToMebibytesFloat(bts uint64) float64 { return float64(bts) / (1024 * 1024) } // getSystemSample gathers a new systemSample. func getSystemSample(now time.Time, lg Logger) *systemSample { s := systemSample{ when: now, numGoroutine: runtime.NumGoroutine(), numCPU: runtime.NumCPU(), } if usage, err := sysinfo.GetUsage(); err == nil { s.usage = usage } else { lg.Warn("unable to usage", map[string]interface{}{ "error": err.Error(), }) } runtime.ReadMemStats(&s.memStats) return &s } type cpuStats struct { used time.Duration fraction float64 // used / (elapsed * numCPU) } // systemStats contains system information for a period of time. type systemStats struct { numGoroutine int allocBytes uint64 heapObjects uint64 user cpuStats system cpuStats gcPauseFraction float64 deltaNumGC uint32 deltaPauseTotal time.Duration minPause time.Duration maxPause time.Duration } // systemSamples is used as the parameter to getSystemStats to avoid mixing up the previous // and current sample. type systemSamples struct { Previous *systemSample Current *systemSample } // getSystemStats combines two systemSamples into a Stats. func getSystemStats(ss systemSamples) systemStats { cur := ss.Current prev := ss.Previous elapsed := cur.when.Sub(prev.when) s := systemStats{ numGoroutine: cur.numGoroutine, allocBytes: cur.memStats.Alloc, heapObjects: cur.memStats.HeapObjects, } // CPU Utilization // For the initial sample (or if prior usage data is unavailable or zero) the calculated CPU utilization will be zero // to ensure accuracy and prevent misleading spikes. totalCPUSeconds := elapsed.Seconds() * float64(cur.numCPU) if prev.usage.User != 0 && cur.usage.User > prev.usage.User { s.user.used = cur.usage.User - prev.usage.User s.user.fraction = s.user.used.Seconds() / totalCPUSeconds } if prev.usage.System != 0 && cur.usage.System > prev.usage.System { s.system.used = cur.usage.System - prev.usage.System s.system.fraction = s.system.used.Seconds() / totalCPUSeconds } // GC Pause Fraction deltaPauseTotalNs := cur.memStats.PauseTotalNs - prev.memStats.PauseTotalNs frac := float64(deltaPauseTotalNs) / float64(elapsed.Nanoseconds()) s.gcPauseFraction = frac // GC Pauses if deltaNumGC := cur.memStats.NumGC - prev.memStats.NumGC; deltaNumGC > 0 { // In case more than 256 pauses have happened between samples // and we are examining a subset of the pauses, we ensure that // the min and max are not on the same side of the average by // using the average as the starting min and max. maxPauseNs := deltaPauseTotalNs / uint64(deltaNumGC) minPauseNs := deltaPauseTotalNs / uint64(deltaNumGC) for i := prev.memStats.NumGC + 1; i <= cur.memStats.NumGC; i++ { pause := cur.memStats.PauseNs[(i+255)%256] if pause > maxPauseNs { maxPauseNs = pause } if pause < minPauseNs { minPauseNs = pause } } s.deltaPauseTotal = time.Duration(deltaPauseTotalNs) * time.Nanosecond s.deltaNumGC = deltaNumGC s.minPause = time.Duration(minPauseNs) * time.Nanosecond s.maxPause = time.Duration(maxPauseNs) * time.Nanosecond } return s } // MergeIntoHarvest implements Harvestable. func (s systemStats) MergeIntoHarvest(h *harvest) { h.Metrics.addValue(heapObjectsAllocated, "", float64(s.heapObjects), forced) h.Metrics.addValue(runGoroutine, "", float64(s.numGoroutine), forced) h.Metrics.addValueExclusive(memoryPhysical, "", bytesToMebibytesFloat(s.allocBytes), 0, forced) h.Metrics.addValueExclusive(cpuUserUtilization, "", s.user.fraction, 0, forced) h.Metrics.addValueExclusive(cpuSystemUtilization, "", s.system.fraction, 0, forced) h.Metrics.addValue(cpuUserTime, "", s.user.used.Seconds(), forced) h.Metrics.addValue(cpuSystemTime, "", s.system.used.Seconds(), forced) h.Metrics.addValueExclusive(gcPauseFraction, "", s.gcPauseFraction, 0, forced) if s.deltaNumGC > 0 { h.Metrics.add(gcPauses, "", metricData{ countSatisfied: float64(s.deltaNumGC), totalTolerated: s.deltaPauseTotal.Seconds(), exclusiveFailed: 0, min: s.minPause.Seconds(), max: s.maxPause.Seconds(), sumSquares: s.deltaPauseTotal.Seconds() * s.deltaPauseTotal.Seconds(), }, forced) } } go-agent-3.42.0/v3/newrelic/sampler_test.go000066400000000000000000000223071510742411500204760ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "runtime" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" "github.com/newrelic/go-agent/v3/internal/sysinfo" ) func TestGetSystemSample(t *testing.T) { now := time.Now() sample := getSystemSample(now, logger.ShimLogger{}) if nil == sample { t.Fatal(sample) } if now != sample.when { t.Error(now, sample.when) } if sample.numGoroutine <= 0 { t.Error(sample.numGoroutine) } if sample.numCPU <= 0 { t.Error(sample.numCPU) } if sample.memStats.HeapObjects == 0 { t.Error(sample.memStats.HeapObjects) } } func TestGetSystemStats(t *testing.T) { baseTime := time.Now() // Test with meaningful differences between samples prev := &systemSample{ when: baseTime, numGoroutine: 10, numCPU: 4, memStats: runtime.MemStats{ Alloc: 50 * 1024 * 1024, // 50MB HeapObjects: 1000, PauseTotalNs: 1000000, // 1ms NumGC: 5, PauseNs: [256]uint64{}, // Will be set below }, usage: sysinfo.Usage{ User: 100 * time.Millisecond, System: 50 * time.Millisecond, }, } current := &systemSample{ when: baseTime.Add(1 * time.Second), numGoroutine: 15, numCPU: 4, memStats: runtime.MemStats{ Alloc: 75 * 1024 * 1024, // 75MB HeapObjects: 1500, PauseTotalNs: 2000000, // 2ms total NumGC: 7, // 2 more GCs PauseNs: [256]uint64{}, // Will be set below }, usage: sysinfo.Usage{ User: 150 * time.Millisecond, System: 80 * time.Millisecond, }, } // Set pause data at correct indices for NumGC transitions from 5 to 7 // The new GCs (6 and 7) should have their pause data at indices (6-1)%256=5 and (7-1)%256=6 current.memStats.PauseNs[5] = 400000 // 400µs for 6th GC current.memStats.PauseNs[6] = 500000 // 500µs for 7th GC stats := getSystemStats(systemSamples{Previous: prev, Current: current}) // Verify basic metrics if stats.numGoroutine != 15 { t.Errorf("Expected numGoroutine=15, got %d", stats.numGoroutine) } if stats.allocBytes != 75*1024*1024 { t.Errorf("Expected allocBytes=78643200, got %d", stats.allocBytes) } if stats.heapObjects != 1500 { t.Errorf("Expected heapObjects=1500, got %d", stats.heapObjects) } // Verify CPU utilization (50ms user over 1s with 4 CPUs = 50ms / 4000ms = 0.0125) expectedUserFraction := 0.0125 if abs(stats.user.fraction-expectedUserFraction) > 0.001 { t.Errorf("Expected user.fraction=%f, got %f", expectedUserFraction, stats.user.fraction) } if stats.user.used != 50*time.Millisecond { t.Errorf("Expected user.used=50ms, got %v", stats.user.used) } // Verify system CPU (30ms system over 1s with 4 CPUs = 30ms / 4000ms = 0.0075) expectedSystemFraction := 0.0075 if abs(stats.system.fraction-expectedSystemFraction) > 0.001 { t.Errorf("Expected system.fraction=%f, got %f", expectedSystemFraction, stats.system.fraction) } if stats.system.used != 30*time.Millisecond { t.Errorf("Expected system.used=30ms, got %v", stats.system.used) } // Verify GC pause fraction (1ms pause over 1s = 0.001) expectedGCFraction := 0.001 if abs(stats.gcPauseFraction-expectedGCFraction) > 0.0001 { t.Errorf("Expected gcPauseFraction=%f, got %f", expectedGCFraction, stats.gcPauseFraction) } // Verify GC pause stats if stats.deltaNumGC != 2 { t.Errorf("Expected deltaNumGC=2, got %d", stats.deltaNumGC) } if stats.deltaPauseTotal != 1*time.Millisecond { t.Errorf("Expected deltaPauseTotal=1ms, got %v", stats.deltaPauseTotal) } if stats.minPause != 400*time.Microsecond { t.Errorf("Expected minPause=400µs, got %v", stats.minPause) } if stats.maxPause != 500*time.Microsecond { t.Errorf("Expected maxPause=500µs, got %v", stats.maxPause) } // Test case with pause greater than average to cover maxPauseNs branch prevMaxTest := &systemSample{ when: baseTime, memStats: runtime.MemStats{ NumGC: 10, PauseTotalNs: 300000, // 300µs total PauseNs: [256]uint64{}, // Will be set below }, usage: sysinfo.Usage{}, } currentMaxTest := &systemSample{ when: baseTime.Add(1 * time.Second), memStats: runtime.MemStats{ NumGC: 12, // 2 new GCs PauseTotalNs: 900000, // 900µs total (600µs delta) PauseNs: [256]uint64{}, // Will be set below }, usage: sysinfo.Usage{}, } // Set pause data at correct indices for NumGC transitions from 10 to 12 // The new GCs (11 and 12) should have their pause data at indices (11-1)%256=10 and (12-1)%256=11 currentMaxTest.memStats.PauseNs[10] = 100000 // 100µs for 11th GC currentMaxTest.memStats.PauseNs[11] = 500000 // 500µs for 12th GC statsMaxTest := getSystemStats(systemSamples{Previous: prevMaxTest, Current: currentMaxTest}) // Average pause would be 600µs / 2 = 300µs // The 500µs pause should trigger the maxPauseNs > pause condition if statsMaxTest.maxPause != 500*time.Microsecond { t.Errorf("Expected maxPause=500µs for max test, got %v", statsMaxTest.maxPause) } if statsMaxTest.minPause != 100*time.Microsecond { t.Errorf("Expected minPause=100µs for max test, got %v", statsMaxTest.minPause) } } func TestGetSystemStatsNoGC(t *testing.T) { baseTime := time.Now() prev := &systemSample{ when: baseTime, memStats: runtime.MemStats{NumGC: 5}, usage: sysinfo.Usage{}, } current := &systemSample{ when: baseTime.Add(1 * time.Second), memStats: runtime.MemStats{NumGC: 5}, // No new GCs usage: sysinfo.Usage{}, } stats := getSystemStats(systemSamples{Previous: prev, Current: current}) // When no GC occurred, these should be zero if stats.deltaNumGC != 0 { t.Errorf("Expected deltaNumGC=0, got %d", stats.deltaNumGC) } if stats.deltaPauseTotal != 0 { t.Errorf("Expected deltaPauseTotal=0, got %v", stats.deltaPauseTotal) } } func TestGetSystemStatsNoCPUUsage(t *testing.T) { baseTime := time.Now() // For the initial sample (or if prior usage data is unavailable or zero) the calculated CPU utilization will be zero // to ensure accuracy and prevent misleading spikes. prev := &systemSample{ when: baseTime, usage: sysinfo.Usage{User: 0, System: 0}, } current := &systemSample{ when: baseTime.Add(1 * time.Second), usage: sysinfo.Usage{User: 100 * time.Millisecond, System: 50 * time.Millisecond}, } stats := getSystemStats(systemSamples{Previous: prev, Current: current}) // CPU stats should be zero when previous usage is 0 if stats.user.used != 0 { t.Errorf("Expected user.used=0, got %v", stats.user.used) } if stats.system.used != 0 { t.Errorf("Expected system.used=0, got %v", stats.system.used) } } func abs(x float64) float64 { if x < 0 { return -x } return x } func TestMetricsCreated(t *testing.T) { now := time.Now() h := newHarvest(now, testHarvestCfgr) stats := systemStats{ heapObjects: 5 * 1000, numGoroutine: 23, allocBytes: 37 * 1024 * 1024, user: cpuStats{ used: 20 * time.Millisecond, fraction: 0.01, }, system: cpuStats{ used: 40 * time.Millisecond, fraction: 0.02, }, gcPauseFraction: 3e-05, deltaNumGC: 2, deltaPauseTotal: 500 * time.Microsecond, minPause: 100 * time.Microsecond, maxPause: 400 * time.Microsecond, } stats.MergeIntoHarvest(h) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: "Memory/Heap/AllocatedObjects", Scope: "", Forced: true, Data: []float64{1, 5000, 5000, 5000, 5000, 25000000}}, {Name: "Memory/Physical", Scope: "", Forced: true, Data: []float64{1, 37, 0, 37, 37, 1369}}, {Name: "CPU/User Time", Scope: "", Forced: true, Data: []float64{1, 0.02, 0.02, 0.02, 0.02, 0.0004}}, {Name: "CPU/System Time", Scope: "", Forced: true, Data: []float64{1, 0.04, 0.04, 0.04, 0.04, 0.0016}}, {Name: "CPU/User/Utilization", Scope: "", Forced: true, Data: []float64{1, 0.01, 0, 0.01, 0.01, 0.0001}}, {Name: "CPU/System/Utilization", Scope: "", Forced: true, Data: []float64{1, 0.02, 0, 0.02, 0.02, 0.0004}}, {Name: "Go/Runtime/Goroutines", Scope: "", Forced: true, Data: []float64{1, 23, 23, 23, 23, 529}}, {Name: "GC/System/Pause Fraction", Scope: "", Forced: true, Data: []float64{1, 3e-05, 0, 3e-05, 3e-05, 9e-10}}, {Name: "GC/System/Pauses", Scope: "", Forced: true, Data: []float64{2, 0.0005, 0, 0.0001, 0.0004, 2.5e-7}}, }) } func TestMetricsCreatedEmpty(t *testing.T) { now := time.Now() h := newHarvest(now, testHarvestCfgr) stats := systemStats{} stats.MergeIntoHarvest(h) expectMetrics(t, h.Metrics, []internal.WantMetric{ {Name: "Memory/Heap/AllocatedObjects", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Memory/Physical", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "CPU/User Time", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "CPU/System Time", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "CPU/User/Utilization", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "CPU/System/Utilization", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "Go/Runtime/Goroutines", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, {Name: "GC/System/Pause Fraction", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, }) } go-agent-3.42.0/v3/newrelic/secure_agent.go000066400000000000000000000105541510742411500204410ustar00rootroot00000000000000package newrelic import ( "net/http" ) const AttributeCsecRoute = "ROUTE" // secureAgent is a global interface point for the nrsecureagent's hooks into the go agent. // The default value for this is a noOpSecurityAgent value, which has null definitions for // the methods. The Go compiler is expected to optimize away all the securityAgent method // calls in this case, effectively removing the hooks from the running agent. // // If the nrsecureagent integration was initialized, it will register a real securityAgent // value in the securityAgent variable instead, thus "activating" the hooks. var secureAgent securityAgent = noOpSecurityAgent{} // GetSecurityAgentInterface returns the securityAgent value // which provides the working interface to the installed // security agent (or to a no-op interface if none were // installed). // // Packages which need to make calls to secureAgent's methods // may obtain the secureAgent value by calling this function. // This avoids exposing the variable itself, so it's not // writable externally and also sets up for the future if this // ends up not being a global variable later. func GetSecurityAgentInterface() securityAgent { return secureAgent } type securityAgent interface { RefreshState(map[string]string) bool DeactivateSecurity() SendEvent(string, ...any) any IsSecurityActive() bool DistributedTraceHeaders(hdrs *http.Request, secureAgentevent any) SendExitEvent(any, error) RequestBodyReadLimit() int } func (app *Application) RegisterSecurityAgent(s securityAgent) { if app != nil && app.app != nil && s != nil { secureAgent = s run, _ := app.app.getState() if run.Reply.IsConnectedToNewRelic() { secureAgent.RefreshState(getLinkedMetaData(app.app)) } } } func (app *Application) UpdateSecurityConfig(s interface{}) { if app == nil || app.app == nil { return } app.app.config.Config.Security = s } func getLinkedMetaData(app *app) map[string]string { runningAppData := make(map[string]string) if app != nil && app.run != nil { runningAppData["hostname"] = app.run.Config.hostname runningAppData["entityName"] = app.run.firstAppName runningAppData["entityGUID"] = app.run.Reply.EntityGUID runningAppData["agentRunId"] = app.run.Reply.RunID.String() runningAppData["accountId"] = app.run.Reply.AccountID } return runningAppData } // noOpSecurityAgent satisfies the secureAgent interface but is a null implementation // that will largely be optimized away at compile time. type noOpSecurityAgent struct { } func (t noOpSecurityAgent) RefreshState(connectionData map[string]string) bool { return false } func (t noOpSecurityAgent) DeactivateSecurity() { } func (t noOpSecurityAgent) SendEvent(caseType string, data ...any) any { return nil } func (t noOpSecurityAgent) IsSecurityActive() bool { return false } func (t noOpSecurityAgent) DistributedTraceHeaders(hdrs *http.Request, secureAgentevent any) { } func (t noOpSecurityAgent) SendExitEvent(secureAgentevent any, err error) { } func (t noOpSecurityAgent) RequestBodyReadLimit() int { return 300 * 1000 } // IsSecurityAgentPresent returns true if there's an actual security agent hooked in to the // Go APM agent, whether or not it's enabled or operating in any particular mode. It returns // false only if the hook-in interface for those functions is a No-Op will null functionality. func IsSecurityAgentPresent() bool { _, isNoOp := secureAgent.(noOpSecurityAgent) return !isNoOp } type BodyBuffer struct { buf []byte isDataTruncated bool } func (b *BodyBuffer) Write(p []byte) (int, error) { l := len(b.buf) if l == secureAgent.RequestBodyReadLimit() { // no room, can't write b.isDataTruncated = true return 0, nil } else if len(p)+l > secureAgent.RequestBodyReadLimit() { // can write, but will truncate to limit end := secureAgent.RequestBodyReadLimit() - l b.buf = append(b.buf, p[:end]...) b.isDataTruncated = true return end, nil } else { // can write all data b.buf = append(b.buf, p...) return len(p), nil } } func (b *BodyBuffer) Len() int { if b == nil { return 0 } return len(b.buf) } func (b *BodyBuffer) read() []byte { if b == nil { return make([]byte, 0) } return b.buf } func (b *BodyBuffer) isBodyTruncated() bool { if b == nil { return false } return b.isDataTruncated } func (b *BodyBuffer) String() (string, bool) { if b == nil { return "", false } return string(b.buf), b.isDataTruncated } go-agent-3.42.0/v3/newrelic/secure_agent_test.go000066400000000000000000000303121510742411500214720ustar00rootroot00000000000000package newrelic import ( "net/http" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) func TestGetSecurityAgentInterface(t *testing.T) { // Test with no-op agent secureAgent = noOpSecurityAgent{} agent := GetSecurityAgentInterface() if agent == nil { t.Error("Expected non-nil security agent interface") } if _, ok := agent.(noOpSecurityAgent); !ok { t.Error("Expected noOpSecurityAgent type") } } func TestNoOpSecurityAgent(t *testing.T) { agent := noOpSecurityAgent{} // Test RefreshState result := agent.RefreshState(map[string]string{"test": "value"}) if result != false { t.Error("Expected RefreshState to return false") } // Test DeactivateSecurity (should not panic) agent.DeactivateSecurity() // Test SendEvent event := agent.SendEvent("test", "data") if event != nil { t.Error("Expected SendEvent to return nil") } // Test IsSecurityActive active := agent.IsSecurityActive() if active != false { t.Error("Expected IsSecurityActive to return false") } // Test DistributedTraceHeaders (should not panic) req, _ := http.NewRequest("GET", "http://example.com", nil) agent.DistributedTraceHeaders(req, nil) // Test SendExitEvent (should not panic) agent.SendExitEvent(nil, nil) // Test RequestBodyReadLimit limit := agent.RequestBodyReadLimit() expected := 300 * 1000 if limit != expected { t.Errorf("Expected RequestBodyReadLimit to return %d, got %d", expected, limit) } } func TestIsSecurityAgentPresent(t *testing.T) { // Save original state originalAgent := secureAgent defer func() { secureAgent = originalAgent }() tests := []struct { name string agent securityAgent expected bool }{ { name: "no-op agent", agent: noOpSecurityAgent{}, expected: false, }, { name: "mock real agent", agent: &mockSecurityAgent{}, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { secureAgent = tt.agent result := IsSecurityAgentPresent() if result != tt.expected { t.Errorf("Expected IsSecurityAgentPresent to return %t with %s", tt.expected, tt.name) } }) } } func TestApplicationRegisterSecurityAgentNilCases(t *testing.T) { // Save original state originalAgent := secureAgent defer func() { secureAgent = originalAgent }() cfgfn := func(cfg *Config) { cfg.Enabled = true } reply := &internal.ConnectReply{ EntityGUID: "test-guid-123", RunID: "123", AccountID: "test-account-id", } tests := []struct { name string app *Application agent securityAgent expectErr string }{ { name: "nil application", app: nil, agent: &mockSecurityAgent{}, expectErr: "Expected secureAgent to remain unchanged with nil application", }, { name: "application with nil app field", app: &Application{app: nil}, agent: &mockSecurityAgent{}, expectErr: "Expected secureAgent to remain unchanged with nil app field", }, { name: "nil agent", app: testApp(func(r *internal.ConnectReply) { *r = *reply }, cfgfn, t).Application, agent: nil, expectErr: "Expected secureAgent to remain unchanged with nil agent", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.app.RegisterSecurityAgent(tt.agent) if secureAgent != originalAgent { t.Error(tt.expectErr) } }) } } func TestApplicationRegisterSecurityAgentConnected(t *testing.T) { // Save original state originalAgent := secureAgent defer func() { secureAgent = originalAgent }() cfgfn := func(cfg *Config) { cfg.Enabled = true } reply := &internal.ConnectReply{ EntityGUID: "test-guid-123", RunID: "123", AccountID: "test-account-id", } app := testApp(func(r *internal.ConnectReply) { *r = *reply }, cfgfn, t) defer app.Shutdown(10 * time.Second) // Mock the app.run state to simulate a connected app if app.app != nil { app.app.run = &appRun{Reply: reply} app.app.run.Config.hostname = "test-hostname" app.app.run.firstAppName = "test-app" } mockAgent := &mockSecurityAgent{} app.RegisterSecurityAgent(mockAgent) // Verify the agent was registered if secureAgent != mockAgent { t.Error("Expected secureAgent to be set to mockAgent") } if !mockAgent.refreshStateCalled { t.Error("Expected RefreshState to be called during registration") } } func TestApplicationRegisterSecurityAgentNotConnected(t *testing.T) { // Save original state originalAgent := secureAgent defer func() { secureAgent = originalAgent }() // Test with app that's not connected to New Relic app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) mockAgent := &mockSecurityAgent{} app.RegisterSecurityAgent(mockAgent) // Verify the agent was registered even if not connected if secureAgent != mockAgent { t.Error("Expected secureAgent to be set to mockAgent even when not connected") } if mockAgent.refreshStateCalled { t.Error("Expected RefreshState to NOT be called during registration") } } func TestApplicationUpdateSecurityConfigNilCases(t *testing.T) { // Test with nil application var app *Application app.UpdateSecurityConfig("test config") // Should not panic // Test with application that has nil app field app = &Application{} app.UpdateSecurityConfig("test config") // Should not panic } func TestApplicationUpdateSecurityConfig(t *testing.T) { app := testApp(nil, ConfigEnabled(true), t) defer app.Shutdown(10 * time.Second) app.UpdateSecurityConfig("test config") if app.app == nil || app.app.config.Config.Security != "test config" { t.Error("Expected security config to be updated") } } func TestGetLinkedMetaDataNilCases(t *testing.T) { // Test with nil app metadata := getLinkedMetaData(nil) if len(metadata) != 0 { t.Error("Expected empty metadata for nil app") } // Test with app that has nil run app := &app{} metadata = getLinkedMetaData(app) if len(metadata) != 0 { t.Error("Expected empty metadata for app with nil run") } } func TestGetLinkedMetaData(t *testing.T) { cfgfn := func(cfg *Config) { cfg.Enabled = true } reply := &internal.ConnectReply{ EntityGUID: "test-guid-123", RunID: "123", AccountID: "test-account-id", } app := testApp(func(r *internal.ConnectReply) { *r = *reply }, cfgfn, t) // Mock the app.run state to simulate a connected app if app.app != nil { app.app.run = &appRun{Reply: reply} app.app.run.Config.hostname = "test-hostname" app.app.run.firstAppName = "test-app" } defer app.Shutdown(10 * time.Second) metadata := getLinkedMetaData(app.app) // Verify metadata is populated with expected fields and values expectedFields := map[string]string{ "hostname": "test-hostname", "entityName": "test-app", "entityGUID": "test-guid-123", "agentRunId": "123", "accountId": "test-account-id", } if len(metadata) != len(expectedFields) { t.Errorf("Expected %d metadata fields, got %d", len(expectedFields), len(metadata)) } for field, expectedValue := range expectedFields { if value, exists := metadata[field]; !exists { t.Errorf("Expected metadata field '%s' to exist", field) } else if value != expectedValue { t.Errorf("Expected metadata field '%s' to have value '%s', got '%s'", field, expectedValue, value) } } } func TestBodyBufferWrite(t *testing.T) { // Save original state originalAgent := secureAgent defer func() { secureAgent = originalAgent }() // Set up mock agent with known limit mockAgent := &mockSecurityAgent{limit: 10} secureAgent = mockAgent tests := []struct { name string initialBuf []byte writeData []byte expectedN int expectedBufLen int expectedBuf string expectedTrunc bool }{ { name: "write within limit", initialBuf: nil, writeData: []byte("hello"), expectedN: 5, expectedBufLen: 5, expectedBuf: "hello", expectedTrunc: false, }, { name: "write exactly to limit", initialBuf: nil, writeData: []byte("1234567890"), expectedN: 10, expectedBufLen: 10, expectedBuf: "1234567890", expectedTrunc: false, }, { name: "write partial when exceeding limit", initialBuf: []byte("12345"), writeData: []byte("abcdefgh"), expectedN: 5, expectedBufLen: 10, expectedBuf: "12345abcde", expectedTrunc: true, }, { name: "write when buffer at limit-1", initialBuf: []byte("123456789"), writeData: []byte("abc"), expectedN: 1, expectedBufLen: 10, expectedBuf: "123456789a", expectedTrunc: true, }, { name: "write when buffer already at limit", initialBuf: []byte("1234567890"), writeData: []byte("x"), expectedN: 0, expectedBufLen: 10, expectedBuf: "1234567890", expectedTrunc: true, }, { name: "write empty data", initialBuf: nil, writeData: []byte{}, expectedN: 0, expectedBufLen: 0, expectedBuf: "", expectedTrunc: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buffer := &BodyBuffer{buf: tt.initialBuf} n, err := buffer.Write(tt.writeData) if err != nil { t.Errorf("Expected no error, got %v", err) } if n != tt.expectedN { t.Errorf("Expected %d bytes written, got %d", tt.expectedN, n) } if len(buffer.buf) != tt.expectedBufLen { t.Errorf("Expected buffer length %d, got %d", tt.expectedBufLen, len(buffer.buf)) } if string(buffer.buf) != tt.expectedBuf { t.Errorf("Expected '%s', got '%s'", tt.expectedBuf, string(buffer.buf)) } if buffer.isDataTruncated != tt.expectedTrunc { t.Errorf("Expected truncated=%t, got %t", tt.expectedTrunc, buffer.isDataTruncated) } }) } } func TestBodyBufferLen(t *testing.T) { // Test with nil buffer var buffer *BodyBuffer if buffer.Len() != 0 { t.Error("Expected Len() to return 0 for nil buffer") } // Test with empty buffer buffer = &BodyBuffer{} if buffer.Len() != 0 { t.Error("Expected Len() to return 0 for empty buffer") } // Test with data buffer.buf = []byte("test") if buffer.Len() != 4 { t.Errorf("Expected Len() to return 4, got %d", buffer.Len()) } } func TestBodyBufferRead(t *testing.T) { // Test with nil buffer var buffer *BodyBuffer data := buffer.read() if len(data) != 0 { t.Error("Expected empty slice for nil buffer") } // Test with buffer containing data buffer = &BodyBuffer{buf: []byte("test")} data = buffer.read() if string(data) != "test" { t.Errorf("Expected 'test', got '%s'", string(data)) } } func TestBodyBufferIsBodyTruncated(t *testing.T) { // Test with nil buffer var buffer *BodyBuffer if buffer.isBodyTruncated() { t.Error("Expected false for nil buffer") } // Test with non-truncated buffer buffer = &BodyBuffer{} if buffer.isBodyTruncated() { t.Error("Expected false for non-truncated buffer") } // Test with truncated buffer buffer.isDataTruncated = true if !buffer.isBodyTruncated() { t.Error("Expected true for truncated buffer") } } func TestBodyBufferString(t *testing.T) { // Test with nil buffer var buffer *BodyBuffer str, truncated := buffer.String() if str != "" || truncated != false { t.Error("Expected empty string and false for nil buffer") } // Test with buffer containing data buffer = &BodyBuffer{ buf: []byte("test data"), isDataTruncated: true, } str, truncated = buffer.String() if str != "test data" { t.Errorf("Expected 'test data', got '%s'", str) } if !truncated { t.Error("Expected truncated to be true") } } // Mock security agent for testing type mockSecurityAgent struct { limit int refreshStateCalled bool refreshStateParams map[string]string } func (m *mockSecurityAgent) RefreshState(params map[string]string) bool { m.refreshStateCalled = true m.refreshStateParams = params return true } func (m *mockSecurityAgent) DeactivateSecurity() {} func (m *mockSecurityAgent) SendEvent(string, ...any) any { return nil } func (m *mockSecurityAgent) IsSecurityActive() bool { return true } func (m *mockSecurityAgent) DistributedTraceHeaders(*http.Request, any) {} func (m *mockSecurityAgent) SendExitEvent(any, error) {} func (m *mockSecurityAgent) RequestBodyReadLimit() int { if m.limit > 0 { return m.limit } return 300 * 1000 } go-agent-3.42.0/v3/newrelic/segments.go000066400000000000000000000301221510742411500176130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" ) // SegmentStartTime is created by Transaction.StartSegmentNow and marks the // beginning of a segment. A segment with a zero-valued SegmentStartTime may // safely be ended. type SegmentStartTime struct { start segmentStartTime thread *thread } // Segment is used to instrument functions, methods, and blocks of code. The // easiest way use Segment is the Transaction.StartSegment method. type Segment struct { StartTime SegmentStartTime Name string } // DatastoreSegment is used to instrument calls to databases and object stores. type DatastoreSegment struct { // StartTime should be assigned using Transaction.StartSegmentNow before // each datastore call is made. StartTime SegmentStartTime // Product, Collection, and Operation are highly recommended as they are // used for aggregate metrics: // // Product is the datastore type. See the constants in // https://github.com/newrelic/go-agent/blob/master/datastore.go. Product // is one of the fields primarily responsible for the grouping of Datastore // metrics. Product DatastoreProduct // Collection is the table or group being operated upon in the datastore, // e.g. "users_table". This becomes the db.collection attribute on Span // events and Transaction Trace segments. Collection is one of the fields // primarily responsible for the grouping of Datastore metrics. Collection string // Operation is the relevant action, e.g. "SELECT" or "GET". Operation is // one of the fields primarily responsible for the grouping of Datastore // metrics. Operation string // The following fields are used for extra metrics and added to instance // data: // // ParameterizedQuery may be set to the query being performed. It must // not contain any raw parameters, only placeholders. ParameterizedQuery string // RawQuery stores the original raw query RawQuery string // QueryParameters may be used to provide query parameters. Care should // be taken to only provide parameters which are not sensitive. // QueryParameters are ignored in high security mode. The keys must contain // fewer than than 255 bytes. The values must be numbers, strings, or // booleans. QueryParameters map[string]interface{} // Host is the name of the server hosting the datastore. Host string // PortPathOrID can represent either the port, path, or id of the // datastore being connected to. PortPathOrID string // DatabaseName is name of database instance where the current query is // being executed. This becomes the db.instance attribute on Span events // and Transaction Trace segments. DatabaseName string // secureAgentEvent is used when vulnerability scanning is enabled to // record security-related information about the datastore operations. secureAgentEvent any } // SetSecureAgentEvent allows integration packages to set the secureAgentEvent // for this datastore segment. That field is otherwise unexported and not available // for other manipulation. func (ds *DatastoreSegment) SetSecureAgentEvent(event any) { ds.secureAgentEvent = event } // GetSecureAgentEvent retrieves the secureAgentEvent previously stored by // a SetSecureAgentEvent method. func (ds *DatastoreSegment) GetSecureAgentEvent() any { return ds.secureAgentEvent } // ExternalSegment instruments external calls. StartExternalSegment is the // recommended way to create ExternalSegments. type ExternalSegment struct { StartTime SegmentStartTime Request *http.Request Response *http.Response // URL is an optional field which can be populated in lieu of Request if // you don't have an http.Request. Either URL or Request must be // populated. If both are populated then Request information takes // priority. URL is parsed using url.Parse so it must include the // protocol scheme (eg. "http://"). URL string // Host is an optional field that is automatically populated from the // Request or URL. It is used for external metrics, transaction trace // segment names, and span event names. Use this field to override the // host in the URL or Request. This field does not override the host in // the "http.url" attribute. Host string // Procedure is an optional field that can be set to the remote // procedure being called. If set, this value will be used in metrics, // transaction trace segment names, and span event names. If unset, the // request's http method is used. Procedure string // Library is an optional field that defaults to "http". It is used for // external metrics and the "component" span attribute. It should be // the framework making the external call. Library string // statusCode is the status code for the response. This value takes // precedence over the status code set on the Response. statusCode *int // secureAgentEvent records security information when vulnerability // scanning is enabled. secureAgentEvent any } // MessageProducerSegment instruments calls to add messages to a queueing system. type MessageProducerSegment struct { StartTime SegmentStartTime // Library is the name of the library instrumented. eg. "RabbitMQ", // "JMS" Library string // DestinationType is the destination type. DestinationType MessageDestinationType // DestinationName is the name of your queue or topic. eg. "UsersQueue". DestinationName string // DestinationTemporary must be set to true if destination is temporary // to improve metric grouping. DestinationTemporary bool } // MessageDestinationType is used for the MessageSegment.DestinationType field. type MessageDestinationType string // These message destination type constants are used in for the // MessageSegment.DestinationType field. const ( MessageQueue MessageDestinationType = "Queue" MessageTopic MessageDestinationType = "Topic" MessageExchange MessageDestinationType = "Exchange" ) // AddAttribute adds a key value pair to the current segment. // // The key must contain fewer than than 255 bytes. The value must be a // number, string, or boolean. func (s *Segment) AddAttribute(key string, val interface{}) { if nil == s { return } addSpanAttr(s.StartTime, key, val) } // End finishes the segment. func (s *Segment) End() { if s == nil { return } if s.StartTime.thread != nil && s.StartTime.thread.thread != nil && s.StartTime.thread.thread.threadID > 0 && IsSecurityAgentPresent() { // async thread secureAgent.SendEvent("NEW_GOROUTINE_END", "") } if err := endBasic(s); err != nil { s.StartTime.thread.logAPIError(err, "end segment", map[string]interface{}{ "name": s.Name, }) } } // AddAttribute adds a key value pair to the current DatastoreSegment. // // The key must contain fewer than than 255 bytes. The value must be a // number, string, or boolean. func (s *DatastoreSegment) AddAttribute(key string, val interface{}) { if nil == s { return } addSpanAttr(s.StartTime, key, val) } // End finishes the datastore segment. func (s *DatastoreSegment) End() { if nil == s { return } if err := endDatastore(s); err != nil { s.StartTime.thread.logAPIError(err, "end datastore segment", map[string]interface{}{ "product": s.Product, "collection": s.Collection, "operation": s.Operation, }) } } // AddAttribute adds a key value pair to the current ExternalSegment. // // The key must contain fewer than than 255 bytes. The value must be a // number, string, or boolean. func (s *ExternalSegment) AddAttribute(key string, val interface{}) { if nil == s { return } addSpanAttr(s.StartTime, key, val) } // End finishes the external segment. func (s *ExternalSegment) End() { if nil == s { return } if err := endExternal(s); err != nil { extraDetails := map[string]interface{}{ "host": s.Host, "procedure": s.Procedure, "library": s.Library, } if s.Request != nil { extraDetails["request.url"] = safeURL(s.Request.URL) } s.StartTime.thread.logAPIError(err, "end external segment", extraDetails) } if ((s.statusCode != nil && *s.statusCode != 404) || (s.Response != nil && s.Response.StatusCode != 404)) && IsSecurityAgentPresent() { secureAgent.SendExitEvent(s.secureAgentEvent, nil) } } // AddAttribute adds a key value pair to the current MessageProducerSegment. // // The key must contain fewer than than 255 bytes. The value must be a // number, string, or boolean. func (s *MessageProducerSegment) AddAttribute(key string, val interface{}) { if nil == s { return } addSpanAttr(s.StartTime, key, val) } // End finishes the message segment. func (s *MessageProducerSegment) End() { if nil == s { return } if err := endMessage(s); err != nil { s.StartTime.thread.logAPIError(err, "end message producer segment", map[string]interface{}{ "library": s.Library, "destination-name": s.DestinationName, }) } } // SetStatusCode sets the status code for the response of this ExternalSegment. // This status code will be included as an attribute on Span Events. If status // code is not set using this method, then the status code found on the // ExternalSegment.Response will be used. // // Use this method when you are creating ExternalSegment manually using either // StartExternalSegment or the ExternalSegment struct directly. Status code is // set automatically when using NewRoundTripper. func (s *ExternalSegment) SetStatusCode(code int) { s.statusCode = &code } // outboundHeaders returns the headers that should be attached to the external // request. func (s *ExternalSegment) outboundHeaders() http.Header { return outboundHeaders(s) } func (s *ExternalSegment) GetOutboundHeaders() http.Header { return s.outboundHeaders() } // SetSecureAgentEvent allows integration packages to set the secureAgentEvent // for this external segment. That field is otherwise unexported and not available // for other manipulation. func (s *ExternalSegment) SetSecureAgentEvent(event any) { s.secureAgentEvent = event } // GetSecureAgentEvent retrieves the secureAgentEvent previously stored by // a SetSecureAgentEvent method. func (s *ExternalSegment) GetSecureAgentEvent() any { return s.secureAgentEvent } // StartSegmentNow starts timing a segment. // // Deprecated: StartSegmentNow is deprecated and will be removed in a future // release. Use Transaction.StartSegmentNow instead. func StartSegmentNow(txn *Transaction) SegmentStartTime { return txn.StartSegmentNow() } // StartSegment instruments segments. // // Deprecated: StartSegment is deprecated and will be removed in a future // release. Use Transaction.StartSegment instead. func StartSegment(txn *Transaction, name string) *Segment { return &Segment{ StartTime: txn.StartSegmentNow(), Name: name, } } // StartExternalSegment starts the instrumentation of an external call and adds // distributed tracing headers to the request. If the Transaction parameter is // nil then StartExternalSegment will look for a Transaction in the request's // context using FromContext. // // Using the same http.Client for all of your external requests? Check out // NewRoundTripper: You may not need to use StartExternalSegment at all! func StartExternalSegment(txn *Transaction, request *http.Request) *ExternalSegment { if nil == txn { txn = transactionFromRequestContext(request) } s := &ExternalSegment{ StartTime: txn.StartSegmentNow(), Request: request, } if IsSecurityAgentPresent() { s.secureAgentEvent = secureAgent.SendEvent("OUTBOUND", request) } if request != nil && request.Header != nil { for key, values := range s.outboundHeaders() { for _, value := range values { request.Header.Set(key, value) } } if IsSecurityAgentPresent() { secureAgent.DistributedTraceHeaders(request, s.secureAgentEvent) } } return s } func addSpanAttr(start SegmentStartTime, key string, val interface{}) { if nil == start.thread { return } validatedVal, err := validateUserAttribute(key, val) if nil != err { start.thread.logAPIError(err, "add segment attribute", map[string]interface{}{}) return } // This call locks the thread for us, so we don't need to. if err := start.thread.AddUserSpanAttribute(key, validatedVal); err != nil { start.thread.logAPIError(err, "add segment attribute", map[string]interface{}{}) } } go-agent-3.42.0/v3/newrelic/serverless.go000066400000000000000000000110031510742411500201600ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "io" "sync" "time" ) const ( // agentLanguage is used in the connect JSON and the Lambda JSON. agentLanguage = "go" lambdaMetadataVersion = 2 ) // serverlessHarvest is used to store and log data when the agent is running in // serverless mode. type serverlessHarvest struct { logger Logger awsExecutionEnv string // The Lambda handler could be using multiple goroutines so we use a // mutex to prevent race conditions. sync.Mutex harvest *harvest } // newServerlessHarvest creates a new serverlessHarvest. func newServerlessHarvest(logger Logger, getEnv func(string) string) *serverlessHarvest { return &serverlessHarvest{ logger: logger, awsExecutionEnv: getEnv("AWS_EXECUTION_ENV"), // We can use dfltHarvestCfgr because // serverless mode doesn't have a connect, and therefore won't // have custom event limits from the server. harvest: newHarvest(time.Now(), dfltHarvestCfgr), } } // Consume adds data to the harvest. func (sh *serverlessHarvest) Consume(data harvestable) { if nil == sh { return } sh.Lock() defer sh.Unlock() data.MergeIntoHarvest(sh.harvest) } func (sh *serverlessHarvest) swapHarvest() *harvest { sh.Lock() defer sh.Unlock() h := sh.harvest sh.harvest = newHarvest(time.Now(), dfltHarvestCfgr) return h } // Write logs the data in the format described by: // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md func (sh *serverlessHarvest) Write(arn string, writer io.Writer) { if nil == sh { return } harvest := sh.swapHarvest() payloads := harvest.Payloads(false) // Note that *json.RawMessage (instead of json.RawMessage) is used to // support older Go versions: https://go-review.googlesource.com/c/go/+/21811/ harvestPayloads := make(map[string]*json.RawMessage, len(payloads)) for _, p := range payloads { agentRunID := "" cmd := p.EndpointMethod() data, err := p.Data(agentRunID, time.Now()) if err != nil { sh.logger.Error("error creating payload json", map[string]interface{}{ "command": cmd, "error": err.Error(), }) continue } if nil == data { continue } // NOTE! This code relies on the fact that each payload is // using a different endpoint method. Sometimes the transaction // events payload might be split, but since there is only one // transaction event per serverless transaction, that's not an // issue. Likewise, if we ever split normal transaction events // apart from synthetics events, the transaction will either be // normal or synthetic, so that won't be an issue. Log an error // if this happens for future defensiveness. if _, ok := harvestPayloads[cmd]; ok { sh.logger.Error("data with duplicate command name lost", map[string]interface{}{ "command": cmd, }) } d := json.RawMessage(data) harvestPayloads[cmd] = &d } if len(harvestPayloads) == 0 { // The harvest may not contain any data if the serverless // transaction was ignored. sh.logger.Debug("go agent serverless harvest contained no payload data", nil) return } data, err := json.Marshal(harvestPayloads) if nil != err { sh.logger.Error("error creating serverless data json", map[string]interface{}{ "error": err.Error(), }) return } var dataBuf bytes.Buffer gz := gzip.NewWriter(&dataBuf) gz.Write(data) gz.Flush() gz.Close() js, err := json.Marshal([]interface{}{ lambdaMetadataVersion, "NR_LAMBDA_MONITORING", struct { MetadataVersion int `json:"metadata_version"` ARN string `json:"arn,omitempty"` ProtocolVersion int `json:"protocol_version"` ExecutionEnvironment string `json:"execution_environment,omitempty"` AgentVersion string `json:"agent_version"` AgentLanguage string `json:"agent_language"` }{ MetadataVersion: lambdaMetadataVersion, ProtocolVersion: procotolVersion, AgentVersion: Version, ExecutionEnvironment: sh.awsExecutionEnv, ARN: arn, AgentLanguage: agentLanguage, }, base64.StdEncoding.EncodeToString(dataBuf.Bytes()), }) if err != nil { sh.logger.Error("error creating serverless json", map[string]interface{}{ "error": err.Error(), }) return } // log json data to stdout if the agent is in debug mode to help troubleshoot lambda issues sh.logger.Debug("harvest data: "+string(js), nil) fmt.Fprintln(writer, string(js)) } go-agent-3.42.0/v3/newrelic/serverless_from_internal.go000066400000000000000000000025641510742411500231130ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "io" "strings" ) // parseServerlessPayload exists for testing. func parseServerlessPayload(data []byte) (metadata, uncompressedData map[string]json.RawMessage, err error) { var arr [4]json.RawMessage if err = json.Unmarshal(data, &arr); nil != err { err = fmt.Errorf("unable to unmarshal serverless data array: %v", err) return } var dataJSON []byte compressed := strings.Trim(string(arr[3]), `"`) if dataJSON, err = decodeUncompress(compressed); nil != err { err = fmt.Errorf("unable to uncompress serverless data: %v", err) return } if err = json.Unmarshal(dataJSON, &uncompressedData); nil != err { err = fmt.Errorf("unable to unmarshal uncompressed serverless data: %v", err) return } if err = json.Unmarshal(arr[2], &metadata); nil != err { err = fmt.Errorf("unable to unmarshal serverless metadata: %v", err) return } return } func decodeUncompress(input string) ([]byte, error) { decoded, err := base64.StdEncoding.DecodeString(input) if nil != err { return nil, err } buf := bytes.NewBuffer(decoded) gz, err := gzip.NewReader(buf) if nil != err { return nil, err } var out bytes.Buffer io.Copy(&out, gz) gz.Close() return out.Bytes(), nil } go-agent-3.42.0/v3/newrelic/serverless_from_internal_test.go000066400000000000000000000140011510742411500241370ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "strings" "testing" ) func createCompressedData(data map[string]interface{}) string { jsonData, _ := json.Marshal(data) var buf bytes.Buffer gz := gzip.NewWriter(&buf) _, err := gz.Write(jsonData) if err != nil { return "" } err = gz.Close() if err != nil { return "" } return base64.StdEncoding.EncodeToString(buf.Bytes()) } func createServerlessPayload(metadata map[string]interface{}, compressedData string) []byte { metadataJSON, _ := json.Marshal(metadata) payload := []interface{}{ nil, nil, json.RawMessage(metadataJSON), json.RawMessage(`"` + compressedData + `"`), } payloadJSON, _ := json.Marshal(payload) return payloadJSON } func TestParseServerlessPayload(t *testing.T) { testData := map[string]interface{}{ "key1": "value1", "key2": 123, } compressedData := createCompressedData(testData) metadata := map[string]interface{}{ "version": "1.0", "type": "serverless", } payloadJSON := createServerlessPayload(metadata, compressedData) resultMetadata, resultData, err := parseServerlessPayload(payloadJSON) if err != nil { t.Errorf("expected nil error, got %v", err) } if len(resultMetadata) != 2 { t.Errorf("expected 2 metadata items, got %d", len(resultMetadata)) } if len(resultData) != 2 { t.Errorf("expected 2 data items, got %d", len(resultData)) } // Verify metadata values var version string err = json.Unmarshal(resultMetadata["version"], &version) if err != nil { t.Fail() } if version != "1.0" { t.Errorf("expected version '1.0', got '%s'", version) } var dataType string err = json.Unmarshal(resultMetadata["type"], &dataType) if err != nil { t.Fail() } if dataType != "serverless" { t.Errorf("expected type 'serverless', got '%s'", dataType) } // Verify data values var key1 string err = json.Unmarshal(resultData["key1"], &key1) if err != nil { t.Fail() } if key1 != "value1" { t.Errorf("expected key1 'value1', got '%s'", key1) } var key2 int err = json.Unmarshal(resultData["key2"], &key2) if err != nil { t.Fail() } if key2 != 123 { t.Errorf("expected key2 123, got %d", key2) } } func TestParseServerlessPayloadErrors(t *testing.T) { tests := []struct { name string input []byte wantErr bool errCheck func(error) bool }{ { name: "invalid JSON", input: []byte(`{"invalid": json}`), wantErr: true, }, { name: "invalid array length", input: func() []byte { invalidArray := []interface{}{nil, nil} data, _ := json.Marshal(invalidArray) return data }(), wantErr: true, }, { name: "invalid base64 data", input: func() []byte { metadata := map[string]interface{}{"version": "1.0"} return createServerlessPayload(metadata, "invalid-base64-!@#") }(), wantErr: true, }, { name: "invalid metadata JSON", input: func() []byte { testData := map[string]interface{}{"key": "value"} compressedData := createCompressedData(testData) payload := []interface{}{ nil, nil, json.RawMessage(`{invalid json}`), json.RawMessage(`"` + compressedData + `"`), } data, _ := json.Marshal(payload) return data }(), wantErr: true, }, { name: "invalid metadata type", input: func() []byte { testData := map[string]interface{}{"key": "value"} compressedData := createCompressedData(testData) payload := []interface{}{ nil, nil, json.RawMessage(`"not a json object"`), json.RawMessage(`"` + compressedData + `"`), } data, _ := json.Marshal(payload) return data }(), wantErr: true, errCheck: func(err error) bool { return strings.Contains(err.Error(), "unable to unmarshal serverless metadata") }, }, { name: "invalid uncompressed data", input: func() []byte { invalidJSON := []byte(`{"invalid": json syntax error}`) var buf bytes.Buffer gz := gzip.NewWriter(&buf) _, err := gz.Write(invalidJSON) if err != nil { return nil } err = gz.Close() if err != nil { return nil } compressedInvalidData := base64.StdEncoding.EncodeToString(buf.Bytes()) metadata := map[string]interface{}{"version": "1.0"} return createServerlessPayload(metadata, compressedInvalidData) }(), wantErr: true, errCheck: func(err error) bool { return strings.Contains(err.Error(), "unable to unmarshal uncompressed serverless data") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, _, err := parseServerlessPayload(tt.input) if (err != nil) != tt.wantErr { t.Errorf("unexpected error state: got error = %v, wantErr = %v", err, tt.wantErr) } if tt.errCheck != nil && err != nil { if !tt.errCheck(err) { t.Errorf("error check failed: %v", err) } } }) } } func TestDecodeUncompress(t *testing.T) { t.Run("successful decode and uncompress", func(t *testing.T) { originalData := []byte(`{"test": "data", "number": 42}`) var buf bytes.Buffer gz := gzip.NewWriter(&buf) _, err := gz.Write(originalData) if err != nil { t.Fail() } err = gz.Close() if err != nil { t.Fail() } encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) result, err := decodeUncompress(encoded) if err != nil { t.Fatalf("expected nil error, got %v", err) } if string(result) != string(originalData) { t.Errorf("expected %s, got %s", string(originalData), string(result)) } }) errorTests := []struct { name string input string }{ { name: "invalid base64", input: "invalid-base64-!@#$%", }, { name: "valid base64 but not gzip", input: base64.StdEncoding.EncodeToString([]byte("not gzip data")), }, { name: "empty input", input: "", }, } for _, tt := range errorTests { t.Run(tt.name, func(t *testing.T) { _, err := decodeUncompress(tt.input) if err == nil { t.Errorf("expected error for input '%s', got nil", tt.input) } }) } } go-agent-3.42.0/v3/newrelic/serverless_test.go000066400000000000000000000052071510742411500212300ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal/logger" ) func serverlessGetenvShim(s string) string { if s == "AWS_EXECUTION_ENV" { return "the-execution-env" } return "" } func TestServerlessHarvest(t *testing.T) { // Test the expected ServerlessHarvest use. sh := newServerlessHarvest(logger.ShimLogger{}, serverlessGetenvShim) event, err := createCustomEvent("myEvent", nil, time.Now()) if nil != err { t.Fatal(err) } sh.Consume(event) buf := &bytes.Buffer{} sh.Write("arn", buf) metadata, data, err := parseServerlessPayload(buf.Bytes()) if nil != err { t.Fatal(err) } if v := string(metadata["metadata_version"]); v != `2` { t.Error(v) } if v := string(metadata["arn"]); v != `"arn"` { t.Error(v) } if v := string(metadata["protocol_version"]); v != `17` { t.Error(v) } if v := string(metadata["execution_environment"]); v != `"the-execution-env"` { t.Error(v) } if v := string(metadata["agent_version"]); v != `"`+Version+`"` { t.Error(v) } if v := string(metadata["agent_language"]); v != `"go"` { t.Error(v) } eventData := string(data["custom_event_data"]) if !strings.Contains(eventData, `"type":"myEvent"`) { t.Error(eventData) } if len(data) != 1 { t.Fatal(data) } // Test that the harvest was replaced with a new harvest. buf = &bytes.Buffer{} sh.Write("arn", buf) if 0 != buf.Len() { t.Error(buf.String()) } } func TestServerlessHarvestNil(t *testing.T) { // The public ServerlessHarvest methods should not panic if the // receiver is nil. var sh *serverlessHarvest event, err := createCustomEvent("myEvent", nil, time.Now()) if nil != err { t.Fatal(err) } sh.Consume(event) buf := &bytes.Buffer{} sh.Write("arn", buf) } func TestServerlessHarvestEmpty(t *testing.T) { // Test that ServerlessHarvest.Write doesn't do anything if the harvest // is empty. sh := newServerlessHarvest(logger.ShimLogger{}, serverlessGetenvShim) buf := &bytes.Buffer{} sh.Write("arn", buf) if 0 != buf.Len() { t.Error(buf.String()) } } func BenchmarkServerless(b *testing.B) { // The JSON creation in ServerlessHarvest.Write has not been optimized. // This benchmark would be useful for doing so. sh := newServerlessHarvest(logger.ShimLogger{}, serverlessGetenvShim) event, err := createCustomEvent("myEvent", nil, time.Now()) if nil != err { b.Fatal(err) } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { sh.Consume(event) buf := &bytes.Buffer{} sh.Write("arn", buf) if buf.Len() == 0 { b.Fatal(buf.String()) } } } go-agent-3.42.0/v3/newrelic/slow_queries.go000066400000000000000000000164561510742411500205250ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "container/heap" "hash/fnv" "time" "github.com/newrelic/go-agent/v3/internal/jsonx" ) type queryParameters map[string]interface{} func vetQueryParameters(params map[string]interface{}) (queryParameters, error) { if nil == params { return nil, nil } // Copying the parameters into a new map is safer than modifying the map // from the customer. vetted := make(map[string]interface{}) var retErr error for key, val := range params { val, err := validateUserAttribute(key, val) if nil != err { retErr = err continue } vetted[key] = val } return queryParameters(vetted), retErr } func (q queryParameters) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('{') w := jsonFieldsWriter{buf: buf} for key, val := range q { writeAttributeValueJSON(&w, key, val) } buf.WriteByte('}') } // https://source.datanerd.us/agents/agent-specs/blob/master/Slow-SQLs-LEGACY.md // slowQueryInstance represents a single datastore call. type slowQueryInstance struct { // Fields populated right after the datastore segment finishes: Duration time.Duration DatastoreMetric string ParameterizedQuery string QueryParameters queryParameters Host string PortPathOrID string DatabaseName string StackTrace stackTrace txnEvent } // Aggregation is performed to avoid reporting multiple slow queries with same // query string. Since some datastore segments may be below the slow query // threshold, the aggregation fields Count, Total, and Min should be taken with // a grain of salt. type slowQuery struct { Count int32 // number of times the query has been observed Total time.Duration // cummulative duration Min time.Duration // minimum observed duration // When Count > 1, slowQueryInstance contains values from the slowest // observation. slowQueryInstance } type slowQueries struct { priorityQueue []*slowQuery // lookup maps query strings to indices in the priorityQueue lookup map[string]int } func (slows *slowQueries) Len() int { return len(slows.priorityQueue) } func (slows *slowQueries) Less(i, j int) bool { pq := slows.priorityQueue return pq[i].Duration < pq[j].Duration } func (slows *slowQueries) Swap(i, j int) { pq := slows.priorityQueue si := pq[i] sj := pq[j] pq[i], pq[j] = pq[j], pq[i] slows.lookup[si.ParameterizedQuery] = j slows.lookup[sj.ParameterizedQuery] = i } // Push and Pop are unused: only heap.Init and heap.Fix are used. func (slows *slowQueries) Push(x interface{}) {} func (slows *slowQueries) Pop() interface{} { return nil } func newSlowQueries(max int) *slowQueries { return &slowQueries{ lookup: make(map[string]int, max), priorityQueue: make([]*slowQuery, 0, max), } } // Merge is used to merge slow queries from the transaction into the harvest. func (slows *slowQueries) Merge(other *slowQueries, txnEvent txnEvent) { for _, s := range other.priorityQueue { cp := *s cp.txnEvent = txnEvent slows.observe(cp) } } // merge aggregates the observations from two slow queries with the same Query. func (slow *slowQuery) merge(other slowQuery) { slow.Count += other.Count slow.Total += other.Total if other.Min < slow.Min { slow.Min = other.Min } if other.Duration > slow.Duration { slow.slowQueryInstance = other.slowQueryInstance } } func (slows *slowQueries) observeInstance(slow slowQueryInstance) { slows.observe(slowQuery{ Count: 1, Total: slow.Duration, Min: slow.Duration, slowQueryInstance: slow, }) } func (slows *slowQueries) insertAtIndex(slow slowQuery, idx int) { cpy := new(slowQuery) *cpy = slow slows.priorityQueue[idx] = cpy slows.lookup[slow.ParameterizedQuery] = idx heap.Fix(slows, idx) } func (slows *slowQueries) observe(slow slowQuery) { // Has the query has previously been observed? if idx, ok := slows.lookup[slow.ParameterizedQuery]; ok { slows.priorityQueue[idx].merge(slow) heap.Fix(slows, idx) return } // Has the collection reached max capacity? if len(slows.priorityQueue) < cap(slows.priorityQueue) { idx := len(slows.priorityQueue) slows.priorityQueue = slows.priorityQueue[0 : idx+1] slows.insertAtIndex(slow, idx) return } // Is this query slower than the existing fastest? fastest := slows.priorityQueue[0] if slow.Duration > fastest.Duration { delete(slows.lookup, fastest.ParameterizedQuery) slows.insertAtIndex(slow, 0) return } } // The third element of the slow query JSON should be a hash of the query // string. This hash may be used by backend services to aggregate queries which // have the have the same query string. It is unknown if this actually used. func makeSlowQueryID(query string) uint32 { h := fnv.New32a() h.Write([]byte(query)) return h.Sum32() } func (slow *slowQuery) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('[') jsonx.AppendString(buf, slow.txnEvent.FinalName) buf.WriteByte(',') // Include request.uri if it is included in any destination. // TODO: Change this to the transaction trace segment destination // once transaction trace segment attribute configuration has been // added. uri, _ := slow.txnEvent.Attrs.GetAgentValue(AttributeRequestURI, destAll) jsonx.AppendString(buf, uri) buf.WriteByte(',') jsonx.AppendInt(buf, int64(makeSlowQueryID(slow.ParameterizedQuery))) buf.WriteByte(',') jsonx.AppendString(buf, slow.ParameterizedQuery) buf.WriteByte(',') jsonx.AppendString(buf, slow.DatastoreMetric) buf.WriteByte(',') jsonx.AppendInt(buf, int64(slow.Count)) buf.WriteByte(',') jsonx.AppendFloat(buf, slow.Total.Seconds()*1000.0) buf.WriteByte(',') jsonx.AppendFloat(buf, slow.Min.Seconds()*1000.0) buf.WriteByte(',') jsonx.AppendFloat(buf, slow.Duration.Seconds()*1000.0) buf.WriteByte(',') w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') if "" != slow.Host { w.stringField("host", slow.Host) } if "" != slow.PortPathOrID { w.stringField("port_path_or_id", slow.PortPathOrID) } if "" != slow.DatabaseName { w.stringField("database_name", slow.DatabaseName) } if nil != slow.StackTrace { w.writerField("backtrace", slow.StackTrace) } if nil != slow.QueryParameters { w.writerField("query_parameters", slow.QueryParameters) } sharedBetterCATIntrinsics(&slow.txnEvent, &w) buf.WriteByte('}') buf.WriteByte(']') } // WriteJSON marshals the collection of slow queries into JSON according to the // schema expected by the collector. // // Note: This JSON does not contain the agentRunID. This is for unknown // historical reasons. Since the agentRunID is included in the url, // its use in the other commands' JSON is redundant (although required). func (slows *slowQueries) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('[') buf.WriteByte('[') for idx, s := range slows.priorityQueue { if idx > 0 { buf.WriteByte(',') } s.WriteJSON(buf) } buf.WriteByte(']') buf.WriteByte(']') } func (slows *slowQueries) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { if 0 == len(slows.priorityQueue) { return nil, nil } estimate := 1024 * len(slows.priorityQueue) buf := bytes.NewBuffer(make([]byte, 0, estimate)) slows.WriteJSON(buf) return buf.Bytes(), nil } func (slows *slowQueries) MergeIntoHarvest(newHarvest *harvest) { } func (slows *slowQueries) EndpointMethod() string { return cmdSlowSQLs } go-agent-3.42.0/v3/newrelic/slow_queries_test.go000066400000000000000000000173151510742411500215570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "math/rand" "strconv" "strings" "testing" "time" ) func TestEmptySlowQueriesData(t *testing.T) { slows := newSlowQueries(maxHarvestSlowSQLs) js, err := slows.Data("agentRunID", time.Now()) if nil != js || nil != err { t.Error(string(js), err) } } func TestSlowQueriesBasic(t *testing.T) { acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/zip/zap", nil) txnEvent := txnEvent{ FinalName: "WebTransaction/Go/hello", Duration: 3 * time.Second, Attrs: attr, BetterCAT: betterCAT{ Enabled: false, }, } txnSlows := newSlowQueries(maxTxnSlowQueries) qParams, err := vetQueryParameters(map[string]interface{}{ strings.Repeat("X", attributeKeyLengthLimit+1): "invalid-key", "invalid-value": struct{}{}, "valid": 123, }) if nil == err { t.Error("expected error") } txnSlows.observeInstance(slowQueryInstance{ Duration: 2 * time.Second, DatastoreMetric: "Datastore/statement/MySQL/users/INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", PortPathOrID: "3306", DatabaseName: "production", StackTrace: nil, QueryParameters: qParams, }) harvestSlows := newSlowQueries(maxHarvestSlowSQLs) harvestSlows.Merge(txnSlows, txnEvent) js, err := harvestSlows.Data("agentRunID", time.Now()) expect := compactJSONString(`[[ [ "WebTransaction/Go/hello", "/zip/zap", 3722056893, "INSERT INTO users (name, age) VALUES ($1, $2)", "Datastore/statement/MySQL/users/INSERT", 1, 2000, 2000, 2000, { "host":"db-server-1", "port_path_or_id":"3306", "database_name":"production", "query_parameters":{ "valid":123 } } ] ]]`) if nil != err { t.Error(err) } if string(js) != expect { t.Error(string(js), expect) } } func TestSlowQueriesExcludeURI(t *testing.T) { c := config{Config: defaultConfig()} c.Attributes.Exclude = []string{"request.uri"} acfg := createAttributeConfig(c, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/zip/zap", nil) txnEvent := txnEvent{ FinalName: "WebTransaction/Go/hello", Duration: 3 * time.Second, Attrs: attr, BetterCAT: betterCAT{ Enabled: false, }, } txnSlows := newSlowQueries(maxTxnSlowQueries) qParams, err := vetQueryParameters(map[string]interface{}{ strings.Repeat("X", attributeKeyLengthLimit+1): "invalid-key", "invalid-value": struct{}{}, "valid": 123, }) if nil == err { t.Error("expected error") } txnSlows.observeInstance(slowQueryInstance{ Duration: 2 * time.Second, DatastoreMetric: "Datastore/statement/MySQL/users/INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", PortPathOrID: "3306", DatabaseName: "production", StackTrace: nil, QueryParameters: qParams, }) harvestSlows := newSlowQueries(maxHarvestSlowSQLs) harvestSlows.Merge(txnSlows, txnEvent) js, err := harvestSlows.Data("agentRunID", time.Now()) expect := compactJSONString(`[[ [ "WebTransaction/Go/hello", "", 3722056893, "INSERT INTO users (name, age) VALUES ($1, $2)", "Datastore/statement/MySQL/users/INSERT", 1, 2000, 2000, 2000, { "host":"db-server-1", "port_path_or_id":"3306", "database_name":"production", "query_parameters":{ "valid":123 } } ] ]]`) if nil != err { t.Error(err) } if string(js) != expect { t.Error(string(js), expect) } } func TestSlowQueriesAggregation(t *testing.T) { max := 50 slows := make([]slowQueryInstance, 3*max) for i := 0; i < max; i++ { num := i + 1 str := strconv.Itoa(num) duration := time.Duration(num) * time.Second slow := slowQueryInstance{ DatastoreMetric: "Datastore/" + str, ParameterizedQuery: str, } slow.Duration = duration slow.txnEvent = txnEvent{ FinalName: "Txn/0" + str, } slows[i*3+0] = slow slow.Duration = duration + (100 * time.Second) slow.txnEvent = txnEvent{ FinalName: "Txn/1" + str, } slows[i*3+1] = slow slow.Duration = duration + (200 * time.Second) slow.txnEvent = txnEvent{ FinalName: "Txn/2" + str, } slows[i*3+2] = slow } sq := newSlowQueries(10) seed := int64(99) // arbitrary fixed seed r := rand.New(rand.NewSource(seed)) perm := r.Perm(max * 3) for _, idx := range perm { sq.observeInstance(slows[idx]) } js, err := sq.Data("agentRunID", time.Now()) expect := compactJSONString(`[[ ["Txn/241","",2296612630,"41","Datastore/41",1,241000,241000,241000,{}], ["Txn/242","",2279835011,"42","Datastore/42",2,384000,142000,242000,{}], ["Txn/243","",2263057392,"43","Datastore/43",2,386000,143000,243000,{}], ["Txn/244","",2380500725,"44","Datastore/44",3,432000,44000,244000,{}], ["Txn/247","",2330167868,"47","Datastore/47",2,394000,147000,247000,{}], ["Txn/245","",2363723106,"45","Datastore/45",2,290000,45000,245000,{}], ["Txn/250","",2212577440,"50","Datastore/50",1,250000,250000,250000,{}], ["Txn/246","",2346945487,"46","Datastore/46",2,392000,146000,246000,{}], ["Txn/249","",2430833582,"49","Datastore/49",3,447000,49000,249000,{}], ["Txn/248","",2447611201,"48","Datastore/48",3,444000,48000,248000,{}] ]]`) if nil != err { t.Error(err) } if string(js) != expect { t.Error(string(js), expect) } } func TestSlowQueriesBetterCAT(t *testing.T) { acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/zip/zap", nil) txnEvent := txnEvent{ FinalName: "WebTransaction/Go/hello", Duration: 3 * time.Second, Attrs: attr, TxnID: "my-txn-id", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, } txnEvent.BetterCAT.Inbound = &payload{ Type: "Browser", App: "caller-app", Account: "caller-account", ID: "caller-id", TransactionID: "caller-parent-id", TracedID: "trace-id", TransportDuration: 2 * time.Second, HasNewRelicTraceInfo: true, } txnEvent.BetterCAT.TransportType = "HTTP" txnSlows := newSlowQueries(maxTxnSlowQueries) qParams, err := vetQueryParameters(map[string]interface{}{ strings.Repeat("X", attributeKeyLengthLimit+1): "invalid-key", "invalid-value": struct{}{}, "valid": 123, }) if nil == err { t.Error("expected error") } txnSlows.observeInstance(slowQueryInstance{ Duration: 2 * time.Second, DatastoreMetric: "Datastore/statement/MySQL/users/INSERT", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", Host: "db-server-1", PortPathOrID: "3306", DatabaseName: "production", StackTrace: nil, QueryParameters: qParams, }) harvestSlows := newSlowQueries(maxHarvestSlowSQLs) harvestSlows.Merge(txnSlows, txnEvent) js, err := harvestSlows.Data("agentRunID", time.Now()) expect := compactJSONString(`[[ [ "WebTransaction/Go/hello", "/zip/zap", 3722056893, "INSERT INTO users (name, age) VALUES ($1, $2)", "Datastore/statement/MySQL/users/INSERT", 1, 2000, 2000, 2000, { "host":"db-server-1", "port_path_or_id":"3306", "database_name":"production", "query_parameters":{"valid":123}, "parent.type": "Browser", "parent.app": "caller-app", "parent.account": "caller-account", "parent.transportDuration": 2, "parent.transportType": "HTTP", "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } ] ]]`) if nil != err { t.Error(err) } if string(js) != expect { t.Error(string(js), expect) } } go-agent-3.42.0/v3/newrelic/span_events.go000066400000000000000000000070601510742411500203200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "time" ) // https://source.datanerd.us/agents/agent-specs/blob/master/Span-Events.md type spanCategory string const ( spanCategoryHTTP spanCategory = "http" spanCategoryDatastore = "datastore" // spanCategoryGeneric is a generic span category. spanCategoryGeneric = "generic" ) // spanEvent represents a span event, necessary to support Distributed Tracing. type spanEvent struct { TraceID string GUID string ParentID string TransactionID string Sampled bool Priority priority Timestamp time.Time Duration time.Duration Name string TxnName string Category spanCategory Component string Kind string IsEntrypoint bool TrustedParentID string TracingVendors string AgentAttributes spanAttributeMap UserAttributes spanAttributeMap } // WriteJSON prepares JSON in the format expected by the collector. func (e *spanEvent) WriteJSON(buf *bytes.Buffer) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('[') buf.WriteByte('{') w.stringField("type", "Span") w.stringField("traceId", e.TraceID) w.stringField("guid", e.GUID) if "" != e.ParentID { w.stringField("parentId", e.ParentID) } w.stringField("transactionId", e.TransactionID) w.boolField("sampled", e.Sampled) w.writerField("priority", e.Priority) w.intField("timestamp", timeToIntMillis(e.Timestamp)) w.floatField("duration", e.Duration.Seconds()) w.stringField("name", e.Name) w.stringField("category", string(e.Category)) if e.IsEntrypoint { w.boolField("nr.entryPoint", true) } if e.Component != "" { w.stringField("component", e.Component) } if e.Kind != "" { w.stringField("span.kind", e.Kind) } if "" != e.TrustedParentID { w.stringField("trustedParentId", e.TrustedParentID) } if "" != e.TracingVendors { w.stringField("tracingVendors", e.TracingVendors) } if "" != e.TxnName { w.stringField("transaction.name", e.TxnName) } buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('{') writeAttrs(buf, e.UserAttributes) buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('{') writeAttrs(buf, e.AgentAttributes) buf.WriteByte('}') buf.WriteByte(']') } func writeAttrs(buf *bytes.Buffer, attrs spanAttributeMap) { w := jsonFieldsWriter{buf: buf} for key, val := range attrs { w.writerField(key, val) } } // MarshalJSON is used for testing. func (e *spanEvent) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, 256)) e.WriteJSON(buf) return buf.Bytes(), nil } type spanEvents struct { *analyticsEvents } func newSpanEvents(max int) *spanEvents { return &spanEvents{ analyticsEvents: newAnalyticsEvents(max), } } func (events *spanEvents) addEventPopulated(e *spanEvent) { events.analyticsEvents.addEvent(analyticsEvent{priority: e.Priority, jsonWriter: e}) } // MergeSpanEvents merges the span events from a transaction into the // harvest's span events. This should only be called if the transaction was // sampled and span events are enabled. func (events *spanEvents) MergeSpanEvents(evts []*spanEvent) { for _, evt := range evts { events.addEventPopulated(evt) } } func (events *spanEvents) MergeIntoHarvest(h *harvest) { h.SpanEvents.mergeFailed(events.analyticsEvents) } func (events *spanEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return events.CollectorJSON(agentRunID) } func (events *spanEvents) EndpointMethod() string { return cmdSpanEvents } go-agent-3.42.0/v3/newrelic/span_events_test.go000066400000000000000000000144701510742411500213620ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "testing" "time" "github.com/newrelic/go-agent/v3/internal" ) func testSpanEventJSON(t *testing.T, e *spanEvent, expect string) { js, err := json.Marshal(e) if nil != err { t.Error(err) return } expect = compactJSONString(expect) if string(js) != expect { t.Errorf("\nexpect=%s\nactual=%s\n", expect, string(js)) } } var ( sampleSpanEvent = spanEvent{ TraceID: "trace-id", GUID: "guid", TransactionID: "txn-id", Sampled: true, Priority: 0.5, Timestamp: timeFromUnixMilliseconds(1488393111000), Duration: 2 * time.Second, Name: "myName", Category: spanCategoryGeneric, IsEntrypoint: true, } ) func TestSpanEventGenericRootMarshal(t *testing.T) { e := sampleSpanEvent testSpanEventJSON(t, &e, `[ { "type":"Span", "traceId":"trace-id", "guid":"guid", "transactionId":"txn-id", "sampled":true, "priority":0.500000, "timestamp":1488393111000, "duration":2, "name":"myName", "category":"generic", "nr.entryPoint":true }, {}, {}]`) } func TestSpanEventDatastoreMarshal(t *testing.T) { e := sampleSpanEvent // Alter sample span event for this test case e.IsEntrypoint = false e.ParentID = "parent-id" e.Category = spanCategoryDatastore e.Kind = "client" e.Component = "mySql" e.AgentAttributes.addString(SpanAttributeDBStatement, "SELECT * from foo") e.AgentAttributes.addString(SpanAttributeDBInstance, "123") e.AgentAttributes.addString(SpanAttributePeerAddress, "{host}:{portPathOrId}") e.AgentAttributes.addString(SpanAttributePeerHostname, "host") expectEvent(t, &e, internal.WantEvent{ Intrinsics: map[string]interface{}{ "type": "Span", "traceId": "trace-id", "guid": "guid", "parentId": "parent-id", "transactionId": "txn-id", "sampled": true, "priority": 0.500000, "timestamp": 1.488393111e+12, "duration": 2, "name": "myName", "category": "datastore", "component": "mySql", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.statement": "SELECT * from foo", "db.instance": "123", "peer.address": "{host}:{portPathOrId}", "peer.hostname": "host", }, }) } func TestSpanEventDatastoreWithoutHostMarshal(t *testing.T) { e := sampleSpanEvent // Alter sample span event for this test case e.IsEntrypoint = false e.ParentID = "parent-id" e.Category = spanCategoryDatastore e.Kind = "client" e.Component = "mySql" e.AgentAttributes.addString(SpanAttributeDBStatement, "SELECT * from foo") e.AgentAttributes.addString(SpanAttributeDBInstance, "123") // According to CHANGELOG.md, as of version 1.5, if `Host` and // `PortPathOrID` are not provided in a Datastore segment, they // do not appear as `"unknown"` in transaction traces and slow // query traces. To maintain parity with the other offerings of // the Go Agent, neither do Span Events. expectEvent(t, &e, internal.WantEvent{ Intrinsics: map[string]interface{}{ "type": "Span", "traceId": "trace-id", "guid": "guid", "parentId": "parent-id", "transactionId": "txn-id", "sampled": true, "priority": 0.500000, "timestamp": 1.488393111e+12, "duration": 2, "name": "myName", "category": "datastore", "component": "mySql", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "db.statement": "SELECT * from foo", "db.instance": "123", }, }) } func TestSpanEventExternalMarshal(t *testing.T) { e := sampleSpanEvent // Alter sample span event for this test case e.ParentID = "parent-id" e.IsEntrypoint = false e.Category = spanCategoryHTTP e.Kind = "client" e.Component = "http" e.AgentAttributes.addString(SpanAttributeHTTPURL, "http://url.com") e.AgentAttributes.addString(SpanAttributeHTTPMethod, "GET") expectEvent(t, &e, internal.WantEvent{ Intrinsics: map[string]interface{}{ "type": "Span", "traceId": "trace-id", "guid": "guid", "parentId": "parent-id", "transactionId": "txn-id", "sampled": true, "priority": 0.500000, "timestamp": 1.488393111e+12, "duration": 2, "name": "myName", "category": "http", "component": "http", "span.kind": "client", }, UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{ "http.url": "http://url.com", "http.method": "GET", }, }) } func TestSpanEventsEndpointMethod(t *testing.T) { events := &spanEvents{} m := events.EndpointMethod() if m != cmdSpanEvents { t.Error(m) } } func TestSpanEventsMerge(t *testing.T) { events := []*spanEvent{ { GUID: "span-1-id", ParentID: "root-span-id", Timestamp: time.Now(), Duration: 3 * time.Millisecond, Name: "span1", Category: spanCategoryGeneric, IsEntrypoint: false, Sampled: true, Priority: 0.7, TransactionID: "txn-id", TraceID: "inbound-trace-id", }, { GUID: "span-2-id", ParentID: "span-1-id", Timestamp: time.Now(), Duration: 3 * time.Millisecond, Name: "span2", Category: spanCategoryGeneric, IsEntrypoint: false, Sampled: true, Priority: 0.7, TransactionID: "txn-id", TraceID: "inbound-trace-id", }, } spanEvents := newSpanEvents(10) spanEvents.MergeSpanEvents(events) expectSpanEvents(t, spanEvents, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "span1", "sampled": true, "priority": 0.7, "category": spanCategoryGeneric, "parentId": "root-span-id", "guid": "span-1-id", "transactionId": "txn-id", "traceId": "inbound-trace-id", }, }, { Intrinsics: map[string]interface{}{ "name": "span2", "sampled": true, "priority": 0.7, "category": spanCategoryGeneric, "parentId": "span-1-id", "guid": "span-2-id", "transactionId": "txn-id", "traceId": "inbound-trace-id", }, }, }) } go-agent-3.42.0/v3/newrelic/sql_driver.go000066400000000000000000000243621510742411500201510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "context" "database/sql/driver" "fmt" "time" ) // SQLDriverSegmentBuilder populates DatastoreSegments for sql.Driver // instrumentation. Use this to instrument a database that is not supported by // an existing integration package (nrmysql, nrpq, and nrsqlite3). See // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // for example use. type SQLDriverSegmentBuilder struct { BaseSegment DatastoreSegment ParseQuery func(segment *DatastoreSegment, query string) ParseDSN func(segment *DatastoreSegment, dataSourceName string) } // InstrumentSQLDriver wraps a driver.Driver, adding instrumentation for exec // and query calls made with a transaction-containing context. Use this to // instrument a database driver that is not supported by an existing integration // package (nrmysql, nrpq, and nrsqlite3). See // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // for example use. func InstrumentSQLDriver(d driver.Driver, bld SQLDriverSegmentBuilder) driver.Driver { return optionalMethodsDriver(&wrapDriver{bld: bld, original: d}) } // InstrumentSQLConnector wraps a driver.Connector, adding instrumentation for // exec and query calls made with a transaction-containing context. Use this to // instrument a database connector that is not supported by an existing // integration package (nrmysql, nrpq, and nrsqlite3). See // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // for example use. func InstrumentSQLConnector(connector driver.Connector, bld SQLDriverSegmentBuilder) driver.Connector { return &wrapConnector{original: connector, bld: bld} } func sendSecureEventSQL(query, args any) any { return secureAgent.SendEvent("SQL", query, args) } func sendSecureEventSQLPrepare(query, obj any) { secureAgent.SendEvent("SQL_PREPARE", query, fmt.Sprintf("%p", obj)) } func sendSecureEventSQLPrepareArgs(args, obj any) any { return secureAgent.SendEvent("SQL_PREPARE_ARGS", args, fmt.Sprintf("%p", obj)) } func (bld SQLDriverSegmentBuilder) useDSN(dsn string) SQLDriverSegmentBuilder { if f := bld.ParseDSN; f != nil { f(&bld.BaseSegment, dsn) } return bld } func (bld SQLDriverSegmentBuilder) useQuery(query string) SQLDriverSegmentBuilder { if f := bld.ParseQuery; f != nil { f(&bld.BaseSegment, query) } return bld } func (bld SQLDriverSegmentBuilder) startSegment(ctx context.Context) DatastoreSegment { return bld.startSegmentAt(ctx, time.Now()) } func (bld SQLDriverSegmentBuilder) startSegmentAt(ctx context.Context, at time.Time) DatastoreSegment { segment := bld.BaseSegment segment.StartTime = FromContext(ctx).startSegmentAt(at) return segment } type wrapDriver struct { bld SQLDriverSegmentBuilder original driver.Driver } type wrapConnector struct { bld SQLDriverSegmentBuilder original driver.Connector } type wrapConn struct { bld SQLDriverSegmentBuilder original driver.Conn } type wrapStmt struct { bld SQLDriverSegmentBuilder original driver.Stmt } func (w *wrapDriver) Open(name string) (driver.Conn, error) { original, err := w.original.Open(name) if err != nil { return nil, err } return optionalMethodsConn(&wrapConn{ original: original, bld: w.bld.useDSN(name), }), nil } // OpenConnector implements DriverContext. func (w *wrapDriver) OpenConnector(name string) (driver.Connector, error) { original, err := w.original.(driver.DriverContext).OpenConnector(name) if err != nil { return nil, err } return &wrapConnector{ original: original, bld: w.bld.useDSN(name), }, nil } func (w *wrapConnector) Connect(ctx context.Context) (driver.Conn, error) { original, err := w.original.Connect(ctx) if err != nil { return nil, err } return optionalMethodsConn(&wrapConn{ bld: w.bld, original: original, }), nil } func (w *wrapConnector) Driver() driver.Driver { return optionalMethodsDriver(&wrapDriver{ bld: w.bld, original: w.original.Driver(), }) } func prepare(original driver.Stmt, err error, bld SQLDriverSegmentBuilder, query string) (driver.Stmt, error) { if err != nil { return nil, err } return optionalMethodsStmt(&wrapStmt{ bld: bld.useQuery(query), original: original, }), nil } func (w *wrapConn) Prepare(query string) (driver.Stmt, error) { original, err := w.original.Prepare(query) if IsSecurityAgentPresent() { sendSecureEventSQLPrepare(query, original) } return prepare(original, err, w.bld, query) } // PrepareContext implements ConnPrepareContext. func (w *wrapConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { original, err := w.original.(driver.ConnPrepareContext).PrepareContext(ctx, query) if IsSecurityAgentPresent() { sendSecureEventSQLPrepare(query, original) } return prepare(original, err, w.bld, query) } func (w *wrapConn) Close() error { return w.original.Close() } func (w *wrapConn) Begin() (driver.Tx, error) { return w.original.Begin() } // BeginTx implements ConnBeginTx. func (w *wrapConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { return w.original.(driver.ConnBeginTx).BeginTx(ctx, opts) } // Exec implements Execer. func (w *wrapConn) Exec(query string, args []driver.Value) (driver.Result, error) { var err error var result driver.Result if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQL(query, args) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } result, err = w.original.(driver.Execer).Exec(query, args) return result, err } // ExecContext implements ExecerContext. func (w *wrapConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { var err error var result driver.Result if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQL(query, args) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } startTime := time.Now() result, err = w.original.(driver.ExecerContext).ExecContext(ctx, query, args) if err != driver.ErrSkip { seg := w.bld.useQuery(query).startSegmentAt(ctx, startTime) seg.End() } return result, err } // CheckNamedValue implements NamedValueChecker. func (w *wrapConn) CheckNamedValue(v *driver.NamedValue) error { return w.original.(driver.NamedValueChecker).CheckNamedValue(v) } // Ping implements Pinger. func (w *wrapConn) Ping(ctx context.Context) error { return w.original.(driver.Pinger).Ping(ctx) } func (w *wrapConn) Query(query string, args []driver.Value) (driver.Rows, error) { var err error var result driver.Rows if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQL(query, args) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } result, err = w.original.(driver.Queryer).Query(query, args) return result, err } // QueryContext implements QueryerContext. func (w *wrapConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { var rows driver.Rows var err error if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQL(query, args) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } startTime := time.Now() rows, err = w.original.(driver.QueryerContext).QueryContext(ctx, query, args) if err != driver.ErrSkip { seg := w.bld.useQuery(query).startSegmentAt(ctx, startTime) seg.End() } return rows, err } // ResetSession implements SessionResetter. func (w *wrapConn) ResetSession(ctx context.Context) error { return w.original.(driver.SessionResetter).ResetSession(ctx) } func (w *wrapStmt) Close() error { return w.original.Close() } func (w *wrapStmt) NumInput() int { return w.original.NumInput() } func (w *wrapStmt) Exec(args []driver.Value) (driver.Result, error) { var result driver.Result var err error if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQLPrepareArgs(args, w.original) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } result, err = w.original.Exec(args) return result, err } func (w *wrapStmt) Query(args []driver.Value) (driver.Rows, error) { var result driver.Rows var err error if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQLPrepareArgs(args, w.original) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } result, err = w.original.Query(args) return result, err } // ColumnConverter implements ColumnConverter. func (w *wrapStmt) ColumnConverter(idx int) driver.ValueConverter { return w.original.(driver.ColumnConverter).ColumnConverter(idx) } // CheckNamedValue implements NamedValueChecker. func (w *wrapStmt) CheckNamedValue(v *driver.NamedValue) error { return w.original.(driver.NamedValueChecker).CheckNamedValue(v) } // ExecContext implements StmtExecContext. func (w *wrapStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { var result driver.Result var err error if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQLPrepareArgs(args, w.original) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } segment := w.bld.startSegment(ctx) result, err = w.original.(driver.StmtExecContext).ExecContext(ctx, args) segment.End() return result, err } // QueryContext implements StmtQueryContext. func (w *wrapStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { var rows driver.Rows var err error if IsSecurityAgentPresent() { secureAgentevent := sendSecureEventSQLPrepareArgs(args, w.original) defer func() { secureAgent.SendExitEvent(secureAgentevent, err) }() } segment := w.bld.startSegment(ctx) rows, err = w.original.(driver.StmtQueryContext).QueryContext(ctx, args) segment.End() return rows, err } var ( _ interface { driver.Driver driver.DriverContext } = &wrapDriver{} _ interface { driver.Connector } = &wrapConnector{} _ interface { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext } = &wrapConn{} _ interface { driver.Stmt driver.ColumnConverter driver.NamedValueChecker driver.StmtExecContext driver.StmtQueryContext } = &wrapStmt{} ) go-agent-3.42.0/v3/newrelic/sql_driver_optional_methods.go000066400000000000000000003211521510742411500235760ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 package newrelic import "database/sql/driver" func optionalMethodsDriver(dv *wrapDriver) driver.Driver { // GENERATED CODE DO NOT MODIFY // This code generated by internal/tools/interface-wrapping var ( i0 int32 = 1 << 0 ) var interfaceSet int32 if _, ok := dv.original.(driver.DriverContext); ok { interfaceSet |= i0 } switch interfaceSet { default: // No optional interfaces implemented return struct { driver.Driver }{dv} case i0: return struct { driver.Driver driver.DriverContext }{dv, dv} } } func optionalMethodsStmt(stmt *wrapStmt) driver.Stmt { // GENERATED CODE DO NOT MODIFY // This code generated by internal/tools/interface-wrapping var ( i0 int32 = 1 << 0 i1 int32 = 1 << 1 i2 int32 = 1 << 2 i3 int32 = 1 << 3 ) var interfaceSet int32 if _, ok := stmt.original.(driver.ColumnConverter); ok { interfaceSet |= i0 } if _, ok := stmt.original.(driver.NamedValueChecker); ok { interfaceSet |= i1 } if _, ok := stmt.original.(driver.StmtExecContext); ok { interfaceSet |= i2 } if _, ok := stmt.original.(driver.StmtQueryContext); ok { interfaceSet |= i3 } switch interfaceSet { default: // No optional interfaces implemented return struct { driver.Stmt }{stmt} case i0: return struct { driver.Stmt driver.ColumnConverter }{stmt, stmt} case i1: return struct { driver.Stmt driver.NamedValueChecker }{stmt, stmt} case i0 | i1: return struct { driver.Stmt driver.ColumnConverter driver.NamedValueChecker }{stmt, stmt, stmt} case i2: return struct { driver.Stmt driver.StmtExecContext }{stmt, stmt} case i0 | i2: return struct { driver.Stmt driver.ColumnConverter driver.StmtExecContext }{stmt, stmt, stmt} case i1 | i2: return struct { driver.Stmt driver.NamedValueChecker driver.StmtExecContext }{stmt, stmt, stmt} case i0 | i1 | i2: return struct { driver.Stmt driver.ColumnConverter driver.NamedValueChecker driver.StmtExecContext }{stmt, stmt, stmt, stmt} case i3: return struct { driver.Stmt driver.StmtQueryContext }{stmt, stmt} case i0 | i3: return struct { driver.Stmt driver.ColumnConverter driver.StmtQueryContext }{stmt, stmt, stmt} case i1 | i3: return struct { driver.Stmt driver.NamedValueChecker driver.StmtQueryContext }{stmt, stmt, stmt} case i0 | i1 | i3: return struct { driver.Stmt driver.ColumnConverter driver.NamedValueChecker driver.StmtQueryContext }{stmt, stmt, stmt, stmt} case i2 | i3: return struct { driver.Stmt driver.StmtExecContext driver.StmtQueryContext }{stmt, stmt, stmt} case i0 | i2 | i3: return struct { driver.Stmt driver.ColumnConverter driver.StmtExecContext driver.StmtQueryContext }{stmt, stmt, stmt, stmt} case i1 | i2 | i3: return struct { driver.Stmt driver.NamedValueChecker driver.StmtExecContext driver.StmtQueryContext }{stmt, stmt, stmt, stmt} case i0 | i1 | i2 | i3: return struct { driver.Stmt driver.ColumnConverter driver.NamedValueChecker driver.StmtExecContext driver.StmtQueryContext }{stmt, stmt, stmt, stmt, stmt} } } func optionalMethodsConn(conn *wrapConn) driver.Conn { // GENERATED CODE DO NOT MODIFY // This code generated by internal/tools/interface-wrapping var ( i0 int32 = 1 << 0 i1 int32 = 1 << 1 i2 int32 = 1 << 2 i3 int32 = 1 << 3 i4 int32 = 1 << 4 i5 int32 = 1 << 5 i6 int32 = 1 << 6 i7 int32 = 1 << 7 i8 int32 = 1 << 8 ) var interfaceSet int32 if _, ok := conn.original.(driver.ConnBeginTx); ok { interfaceSet |= i0 } if _, ok := conn.original.(driver.ConnPrepareContext); ok { interfaceSet |= i1 } if _, ok := conn.original.(driver.Execer); ok { interfaceSet |= i2 } if _, ok := conn.original.(driver.ExecerContext); ok { interfaceSet |= i3 } if _, ok := conn.original.(driver.NamedValueChecker); ok { interfaceSet |= i4 } if _, ok := conn.original.(driver.Pinger); ok { interfaceSet |= i5 } if _, ok := conn.original.(driver.Queryer); ok { interfaceSet |= i6 } if _, ok := conn.original.(driver.QueryerContext); ok { interfaceSet |= i7 } if _, ok := conn.original.(driver.SessionResetter); ok { interfaceSet |= i8 } switch interfaceSet { default: // No optional interfaces implemented return struct { driver.Conn }{conn} case i0: return struct { driver.Conn driver.ConnBeginTx }{conn, conn} case i1: return struct { driver.Conn driver.ConnPrepareContext }{conn, conn} case i0 | i1: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext }{conn, conn, conn} case i2: return struct { driver.Conn driver.Execer }{conn, conn} case i0 | i2: return struct { driver.Conn driver.ConnBeginTx driver.Execer }{conn, conn, conn} case i1 | i2: return struct { driver.Conn driver.ConnPrepareContext driver.Execer }{conn, conn, conn} case i0 | i1 | i2: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer }{conn, conn, conn, conn} case i3: return struct { driver.Conn driver.ExecerContext }{conn, conn} case i0 | i3: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext }{conn, conn, conn} case i1 | i3: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext }{conn, conn, conn} case i0 | i1 | i3: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext }{conn, conn, conn, conn} case i2 | i3: return struct { driver.Conn driver.Execer driver.ExecerContext }{conn, conn, conn} case i0 | i2 | i3: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext }{conn, conn, conn, conn} case i1 | i2 | i3: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext }{conn, conn, conn, conn} case i0 | i1 | i2 | i3: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext }{conn, conn, conn, conn, conn} case i4: return struct { driver.Conn driver.NamedValueChecker }{conn, conn} case i0 | i4: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker }{conn, conn, conn} case i1 | i4: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker }{conn, conn, conn} case i0 | i1 | i4: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker }{conn, conn, conn, conn} case i2 | i4: return struct { driver.Conn driver.Execer driver.NamedValueChecker }{conn, conn, conn} case i0 | i2 | i4: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker }{conn, conn, conn, conn} case i1 | i2 | i4: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker }{conn, conn, conn, conn} case i0 | i1 | i2 | i4: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker }{conn, conn, conn, conn, conn} case i3 | i4: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn} case i0 | i3 | i4: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn} case i1 | i3 | i4: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn} case i0 | i1 | i3 | i4: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn, conn} case i2 | i3 | i4: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn} case i0 | i2 | i3 | i4: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker }{conn, conn, conn, conn, conn, conn} case i5: return struct { driver.Conn driver.Pinger }{conn, conn} case i0 | i5: return struct { driver.Conn driver.ConnBeginTx driver.Pinger }{conn, conn, conn} case i1 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger }{conn, conn, conn} case i0 | i1 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger }{conn, conn, conn, conn} case i2 | i5: return struct { driver.Conn driver.Execer driver.Pinger }{conn, conn, conn} case i0 | i2 | i5: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger }{conn, conn, conn, conn} case i1 | i2 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger }{conn, conn, conn, conn} case i0 | i1 | i2 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger }{conn, conn, conn, conn, conn} case i3 | i5: return struct { driver.Conn driver.ExecerContext driver.Pinger }{conn, conn, conn} case i0 | i3 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger }{conn, conn, conn, conn} case i1 | i3 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger }{conn, conn, conn, conn} case i0 | i1 | i3 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger }{conn, conn, conn, conn, conn} case i2 | i3 | i5: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger }{conn, conn, conn, conn} case i0 | i2 | i3 | i5: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger }{conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger }{conn, conn, conn, conn, conn, conn} case i4 | i5: return struct { driver.Conn driver.NamedValueChecker driver.Pinger }{conn, conn, conn} case i0 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn} case i1 | i4 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn} case i0 | i1 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i2 | i4 | i5: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn} case i0 | i2 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn, conn} case i3 | i4 | i5: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn} case i0 | i3 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger }{conn, conn, conn, conn, conn, conn, conn} case i6: return struct { driver.Conn driver.Queryer }{conn, conn} case i0 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Queryer }{conn, conn, conn} case i1 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Queryer }{conn, conn, conn} case i0 | i1 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Queryer }{conn, conn, conn, conn} case i2 | i6: return struct { driver.Conn driver.Execer driver.Queryer }{conn, conn, conn} case i0 | i2 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Queryer }{conn, conn, conn, conn} case i1 | i2 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Queryer }{conn, conn, conn, conn} case i0 | i1 | i2 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Queryer }{conn, conn, conn, conn, conn} case i3 | i6: return struct { driver.Conn driver.ExecerContext driver.Queryer }{conn, conn, conn} case i0 | i3 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Queryer }{conn, conn, conn, conn} case i1 | i3 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Queryer }{conn, conn, conn, conn} case i0 | i1 | i3 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Queryer }{conn, conn, conn, conn, conn} case i2 | i3 | i6: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Queryer }{conn, conn, conn, conn} case i0 | i2 | i3 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i2 | i3 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer }{conn, conn, conn, conn, conn, conn} case i4 | i6: return struct { driver.Conn driver.NamedValueChecker driver.Queryer }{conn, conn, conn} case i0 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn} case i1 | i4 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn} case i0 | i1 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i2 | i4 | i6: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn} case i0 | i2 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i2 | i4 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn, conn} case i3 | i4 | i6: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn} case i0 | i3 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i3 | i4 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i6: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i5 | i6: return struct { driver.Conn driver.Pinger driver.Queryer }{conn, conn, conn} case i0 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.Queryer }{conn, conn, conn, conn} case i1 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.Queryer }{conn, conn, conn, conn} case i0 | i1 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i2 | i5 | i6: return struct { driver.Conn driver.Execer driver.Pinger driver.Queryer }{conn, conn, conn, conn} case i0 | i2 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i2 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i3 | i5 | i6: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn} case i0 | i3 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i3 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i6: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i6: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn} case i0 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i1 | i4 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i6: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i6: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i6: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer }{conn, conn, conn, conn, conn, conn, conn, conn} case i7: return struct { driver.Conn driver.QueryerContext }{conn, conn} case i0 | i7: return struct { driver.Conn driver.ConnBeginTx driver.QueryerContext }{conn, conn, conn} case i1 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.QueryerContext }{conn, conn, conn} case i0 | i1 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.QueryerContext }{conn, conn, conn, conn} case i2 | i7: return struct { driver.Conn driver.Execer driver.QueryerContext }{conn, conn, conn} case i0 | i2 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.QueryerContext }{conn, conn, conn, conn} case i1 | i2 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i1 | i2 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.QueryerContext }{conn, conn, conn, conn, conn} case i3 | i7: return struct { driver.Conn driver.ExecerContext driver.QueryerContext }{conn, conn, conn} case i0 | i3 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn} case i1 | i3 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn} case i0 | i1 | i3 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn, conn} case i2 | i3 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn} case i0 | i2 | i3 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i2 | i3 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i4 | i7: return struct { driver.Conn driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn} case i0 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn} case i1 | i4 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn} case i0 | i1 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i2 | i4 | i7: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn} case i0 | i2 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i2 | i4 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i3 | i4 | i7: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn} case i0 | i3 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i3 | i4 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i5 | i7: return struct { driver.Conn driver.Pinger driver.QueryerContext }{conn, conn, conn} case i0 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.QueryerContext }{conn, conn, conn, conn} case i1 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn} case i0 | i1 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i2 | i5 | i7: return struct { driver.Conn driver.Execer driver.Pinger driver.QueryerContext }{conn, conn, conn, conn} case i0 | i2 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i2 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i3 | i5 | i7: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn} case i0 | i3 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i3 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i7: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn} case i0 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i4 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i7: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i7: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i6 | i7: return struct { driver.Conn driver.Queryer driver.QueryerContext }{conn, conn, conn} case i0 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i1 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i1 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i2 | i6 | i7: return struct { driver.Conn driver.Execer driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i2 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i2 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i3 | i6 | i7: return struct { driver.Conn driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i3 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i3 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i6 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i4 | i6 | i7: return struct { driver.Conn driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i4 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i6 | i7: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i6 | i7: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i5 | i6 | i7: return struct { driver.Conn driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn} case i0 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i1 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i1 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i2 | i5 | i6 | i7: return struct { driver.Conn driver.Execer driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i2 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i3 | i5 | i6 | i7: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i6 | i7: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn} case i0 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i1 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i6 | i7: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i8: return struct { driver.Conn driver.SessionResetter }{conn, conn} case i0 | i8: return struct { driver.Conn driver.ConnBeginTx driver.SessionResetter }{conn, conn, conn} case i1 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.SessionResetter }{conn, conn, conn} case i0 | i1 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.SessionResetter }{conn, conn, conn, conn} case i2 | i8: return struct { driver.Conn driver.Execer driver.SessionResetter }{conn, conn, conn} case i0 | i2 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.SessionResetter }{conn, conn, conn, conn} case i1 | i2 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i2 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.SessionResetter }{conn, conn, conn, conn, conn} case i3 | i8: return struct { driver.Conn driver.ExecerContext driver.SessionResetter }{conn, conn, conn} case i0 | i3 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn} case i1 | i3 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i3 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i2 | i3 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i2 | i3 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i2 | i3 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i4 | i8: return struct { driver.Conn driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn} case i0 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn} case i1 | i4 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i2 | i4 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn} case i0 | i2 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i2 | i4 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i3 | i4 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn} case i0 | i3 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i3 | i4 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i5 | i8: return struct { driver.Conn driver.Pinger driver.SessionResetter }{conn, conn, conn} case i0 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.SessionResetter }{conn, conn, conn, conn} case i1 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i2 | i5 | i8: return struct { driver.Conn driver.Execer driver.Pinger driver.SessionResetter }{conn, conn, conn, conn} case i0 | i2 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i2 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i3 | i5 | i8: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn} case i0 | i3 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i3 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn} case i0 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i4 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i6 | i8: return struct { driver.Conn driver.Queryer driver.SessionResetter }{conn, conn, conn} case i0 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i1 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i2 | i6 | i8: return struct { driver.Conn driver.Execer driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i2 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i2 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i3 | i6 | i8: return struct { driver.Conn driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i3 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i3 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i6 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i4 | i6 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i4 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i6 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i6 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i5 | i6 | i8: return struct { driver.Conn driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn} case i0 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i5 | i6 | i8: return struct { driver.Conn driver.Execer driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i5 | i6 | i8: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i6 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i6 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i7 | i8: return struct { driver.Conn driver.QueryerContext driver.SessionResetter }{conn, conn, conn} case i0 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i1 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i1 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i2 | i7 | i8: return struct { driver.Conn driver.Execer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i2 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i2 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i2 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i3 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i3 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i3 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i3 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i3 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i3 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i4 | i7 | i8: return struct { driver.Conn driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i4 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i4 | i7 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i5 | i7 | i8: return struct { driver.Conn driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i5 | i7 | i8: return struct { driver.Conn driver.Execer driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i5 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i7 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i6 | i7 | i8: return struct { driver.Conn driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn} case i0 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i1 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i1 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i2 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i2 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i2 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i3 | i6 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i4 | i6 | i7 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i5 | i6 | i7 | i8: return struct { driver.Conn driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn} case i0 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i1 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i1 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i2 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i2 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn} case i0 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i1 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn} case i0 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i2 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i2 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i1 | i2 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn} case i0 | i1 | i2 | i3 | i4 | i5 | i6 | i7 | i8: return struct { driver.Conn driver.ConnBeginTx driver.ConnPrepareContext driver.Execer driver.ExecerContext driver.NamedValueChecker driver.Pinger driver.Queryer driver.QueryerContext driver.SessionResetter }{conn, conn, conn, conn, conn, conn, conn, conn, conn, conn} } } go-agent-3.42.0/v3/newrelic/sql_driver_test.go000066400000000000000000000237001510742411500212030ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.10 // +build go1.10 package newrelic import ( "context" "database/sql/driver" "strings" "testing" "github.com/newrelic/go-agent/v3/internal" ) var ( driverTestMetrics = []internal.WantMetric{ {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransaction/Go/hello", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, {Name: "OtherTransactionTotalTime/Go/hello", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/MySQL/allOther", Scope: "", Forced: true, Data: nil}, {Name: "Datastore/operation/MySQL/myoperation", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/mycollection/myoperation", Scope: "", Forced: false, Data: nil}, {Name: "Datastore/statement/MySQL/mycollection/myoperation", Scope: "OtherTransaction/Go/hello", Forced: false, Data: nil}, {Name: "Datastore/instance/MySQL/myhost/myport", Scope: "", Forced: false, Data: nil}, } ) type testDriver struct{} type testConnector struct{} type testConn struct{} type testStmt struct{} func (d testDriver) OpenConnector(name string) (driver.Connector, error) { return testConnector{}, nil } func (d testDriver) Open(name string) (driver.Conn, error) { return testConn{}, nil } func (c testConnector) Connect(context.Context) (driver.Conn, error) { return testConn{}, nil } func (c testConnector) Driver() driver.Driver { return testDriver{} } func (c testConn) Prepare(query string) (driver.Stmt, error) { return testStmt{}, nil } func (c testConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { return testStmt{}, nil } func (c testConn) Close() error { return nil } func (c testConn) Begin() (driver.Tx, error) { return nil, nil } func (c testConn) ExecContext(context.Context, string, []driver.NamedValue) (driver.Result, error) { return nil, nil } func (c testConn) QueryContext(context.Context, string, []driver.NamedValue) (driver.Rows, error) { return nil, nil } func (s testStmt) Close() error { return nil } func (s testStmt) NumInput() int { return 0 } func (s testStmt) Exec(args []driver.Value) (driver.Result, error) { return nil, nil } func (s testStmt) Query(args []driver.Value) (driver.Rows, error) { return nil, nil } func (s testStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { return nil, nil } func (s testStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { return nil, nil } var ( testBuilder = SQLDriverSegmentBuilder{ BaseSegment: DatastoreSegment{ Product: DatastoreMySQL, }, ParseDSN: func(segment *DatastoreSegment, dsn string) { fields := strings.Split(dsn, ",") segment.Host = fields[0] segment.PortPathOrID = fields[1] segment.DatabaseName = fields[2] }, ParseQuery: func(segment *DatastoreSegment, query string) { fields := strings.Split(query, ",") segment.Operation = fields[0] segment.Collection = fields[1] }, } ) func TestDriverStmtExecContext(t *testing.T) { // Test that driver.Stmt.ExecContext calls get instrumented. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) dr := InstrumentSQLDriver(testDriver{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := dr.Open("myhost,myport,mydatabase") stmt, _ := conn.Prepare("myoperation,mycollection") ctx := NewContext(context.Background(), txn) stmt.(driver.StmtExecContext).ExecContext(ctx, nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestDriverStmtQueryContext(t *testing.T) { // Test that driver.Stmt.PrepareContext calls get instrumented. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) dr := InstrumentSQLDriver(testDriver{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := dr.Open("myhost,myport,mydatabase") stmt, _ := conn.(driver.ConnPrepareContext).PrepareContext(nil, "myoperation,mycollection") ctx := NewContext(context.Background(), txn) stmt.(driver.StmtQueryContext).QueryContext(ctx, nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestDriverConnExecContext(t *testing.T) { // Test that driver.Conn.ExecContext calls get instrumented. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) dr := InstrumentSQLDriver(testDriver{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := dr.Open("myhost,myport,mydatabase") ctx := NewContext(context.Background(), txn) conn.(driver.ExecerContext).ExecContext(ctx, "myoperation,mycollection", nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestDriverConnQueryContext(t *testing.T) { // Test that driver.Conn.QueryContext calls get instrumented. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) dr := InstrumentSQLDriver(testDriver{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := dr.Open("myhost,myport,mydatabase") ctx := NewContext(context.Background(), txn) conn.(driver.QueryerContext).QueryContext(ctx, "myoperation,mycollection", nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestDriverContext(t *testing.T) { // Test that driver.OpenConnector returns an instrumented connector. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) dr := InstrumentSQLDriver(testDriver{}, testBuilder) txn := app.StartTransaction("hello") connector, _ := dr.(driver.DriverContext).OpenConnector("myhost,myport,mydatabase") conn, _ := connector.Connect(nil) ctx := NewContext(context.Background(), txn) conn.(driver.ExecerContext).ExecContext(ctx, "myoperation,mycollection", nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestInstrumentSQLConnector(t *testing.T) { // Test that connections returned by an instrumented driver.Connector // are instrumented. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) bld := testBuilder bld.BaseSegment.Host = "myhost" bld.BaseSegment.PortPathOrID = "myport" bld.BaseSegment.DatabaseName = "mydatabase" connector := InstrumentSQLConnector(testConnector{}, bld) txn := app.StartTransaction("hello") conn, _ := connector.Connect(nil) ctx := NewContext(context.Background(), txn) conn.(driver.ExecerContext).ExecContext(ctx, "myoperation,mycollection", nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } func TestConnectorToDriver(t *testing.T) { // Test that driver.Connector.Driver returns an instrumented Driver. app := testApp(nil, ConfigDistributedTracerEnabled(false), t) connector := InstrumentSQLConnector(testConnector{}, testBuilder) txn := app.StartTransaction("hello") dr := connector.Driver() conn, _ := dr.Open("myhost,myport,mydatabase") ctx := NewContext(context.Background(), txn) conn.(driver.ExecerContext).ExecContext(ctx, "myoperation,mycollection", nil) txn.End() app.ExpectMetrics(t, driverTestMetrics) } type testConnectorErr struct { testConnector } func (c testConnectorErr) Connect(context.Context) (driver.Conn, error) { return testConnErr{}, nil } type testConnErr struct { testConn } func (c testConnErr) QueryContext(context.Context, string, []driver.NamedValue) (driver.Rows, error) { return nil, driver.ErrSkip } func (c testConnErr) ExecContext(context.Context, string, []driver.NamedValue) (driver.Result, error) { return nil, driver.ErrSkip } // Ensure that if the driver used returns driver.ErrSkip that spans still have correct parentage func TestExecContextErrSkipReturned(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) connector := InstrumentSQLConnector(testConnectorErr{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := connector.Connect(nil) ctx := NewContext(context.Background(), txn) conn.(driver.ExecerContext).ExecContext(ctx, "myoperation,mycollection", nil) txn.StartSegment("second").End() txn.End() parentGUID := "4981855ad8681d0d" app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/second", "parentId": parentGUID, "category": "generic", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": parentGUID, "nr.entryPoint": true, "category": "generic", "transaction.name": "OtherTransaction/Go/hello", }, AgentAttributes: map[string]interface{}{}, }, }) } // Ensure that if the driver used returns driver.ErrSkip that spans still have correct parentage func TestQueryContextErrSkipReturned(t *testing.T) { app := testApp(distributedTracingReplyFields, enableBetterCAT, t) connector := InstrumentSQLConnector(testConnectorErr{}, testBuilder) txn := app.StartTransaction("hello") conn, _ := connector.Connect(nil) ctx := NewContext(context.Background(), txn) conn.(driver.QueryerContext).QueryContext(ctx, "myoperation,mycollection", nil) txn.StartSegment("second").End() txn.End() parentGUID := "4981855ad8681d0d" app.ExpectSpanEvents(t, []internal.WantEvent{ { Intrinsics: map[string]interface{}{ "name": "Custom/second", "parentId": parentGUID, "category": "generic", }, AgentAttributes: map[string]interface{}{}, }, { Intrinsics: map[string]interface{}{ "name": "OtherTransaction/Go/hello", "guid": parentGUID, "nr.entryPoint": true, "category": "generic", "transaction.name": "OtherTransaction/Go/hello", }, AgentAttributes: map[string]interface{}{}, }, }) } // Ensure we don't panic if the txn is nil func TestSQLNoTxnNoCry(t *testing.T) { connector := InstrumentSQLConnector(testConnector{}, testBuilder) conn, _ := connector.Connect(nil) conn.(driver.QueryerContext).QueryContext(context.Background(), "myoperation,mycollection", nil) } go-agent-3.42.0/v3/newrelic/sqlparse/000077500000000000000000000000001510742411500172735ustar00rootroot00000000000000go-agent-3.42.0/v3/newrelic/sqlparse/sqlparse.go000066400000000000000000000045221510742411500214570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sqlparse import ( "regexp" "strings" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func extractTable(s string) string { s = extractTableRegex.ReplaceAllString(s, "") if idx := strings.Index(s, "."); idx > 0 { s = s[idx+1:] } return s } var ( basicTable = `[^)(\]\[\}\{\s,;]+` enclosedTable = `[\[\(\{]` + `\s*` + basicTable + `\s*` + `[\]\)\}]` tablePattern = `(` + `\s+` + basicTable + `|` + `\s*` + enclosedTable + `)` extractTableRegex = regexp.MustCompile(`[\s` + "`" + `"'\(\)\{\}\[\]]*`) updateRegex = regexp.MustCompile(`(?is)^update(?:\s+(?:low_priority|ignore|or|rollback|abort|replace|fail|only))*` + tablePattern) sqlOperations = map[string]*regexp.Regexp{ "select": regexp.MustCompile(`(?is)^.*\sfrom` + tablePattern), "delete": regexp.MustCompile(`(?is)^.*\sfrom` + tablePattern), "insert": regexp.MustCompile(`(?is)^.*\sinto?` + tablePattern), "update": updateRegex, "call": nil, "create": nil, "drop": nil, "show": nil, "set": nil, "exec": nil, "execute": nil, "alter": nil, "commit": nil, "rollback": nil, } firstWordRegex = regexp.MustCompile(`^\w+`) cCommentRegex = regexp.MustCompile(`(?is)/\*.*?\*/`) lineCommentRegex = regexp.MustCompile(`(?im)(?:--|#).*?$`) sqlPrefixRegex = regexp.MustCompile(`^[\s;]*`) ) // ParseQuery parses table and operation from the SQL query string. It is // a helper meant to be used when writing database/sql driver instrumentation. // Check out full example usage here: // https://github.com/newrelic/go-agent/blob/master/v3/integrations/nrmysql/nrmysql.go // // ParseQuery is designed to work with MySQL, Postgres, and SQLite drivers. // Ability to correctly parse queries for other SQL databases is not // guaranteed. func ParseQuery(segment *newrelic.DatastoreSegment, query string) { s := cCommentRegex.ReplaceAllString(query, "") s = lineCommentRegex.ReplaceAllString(s, "") s = sqlPrefixRegex.ReplaceAllString(s, "") op := strings.ToLower(firstWordRegex.FindString(s)) if rg, ok := sqlOperations[op]; ok { segment.Operation = op segment.RawQuery = query if nil != rg { if m := rg.FindStringSubmatch(s); len(m) > 1 { segment.Collection = extractTable(m[1]) } } } } go-agent-3.42.0/v3/newrelic/sqlparse/sqlparse_test.go000066400000000000000000000120121510742411500225070ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package sqlparse import ( "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" newrelic "github.com/newrelic/go-agent/v3/newrelic" ) type sqlTestcase struct { Input string `json:"input"` Operation string `json:"operation"` Table string `json:"table"` } func (tc sqlTestcase) test(t *testing.T) { var segment newrelic.DatastoreSegment ParseQuery(&segment, tc.Input) if tc.Operation == "other" { // Allow for matching of Operation "other" to "" if segment.Operation != "" { t.Errorf("operation mismatch query='%s' wanted='%s' got='%s'", tc.Input, tc.Operation, segment.Operation) } } else if segment.Operation != tc.Operation { t.Errorf("operation mismatch query='%s' wanted='%s' got='%s'", tc.Input, tc.Operation, segment.Operation) } // The Go agent subquery behavior does not match the PHP Agent. if tc.Table == "(subquery)" { return } if tc.Table != segment.Collection { t.Errorf("table mismatch query='%s' wanted='%s' got='%s'", tc.Input, tc.Table, segment.Collection) } } func TestParseSQLCrossAgent(t *testing.T) { var tcs []sqlTestcase err := crossagent.ReadJSON("sql_parsing.json", &tcs) if err != nil { t.Fatal(err) } for _, tc := range tcs { tc.test(t) } } func TestParseSQLSubQuery(t *testing.T) { for _, tc := range []sqlTestcase{ {Input: "SELECT * FROM (SELECT * FROM foobar)", Operation: "select", Table: "foobar"}, {Input: "SELECT * FROM (SELECT * FROM foobar) WHERE x > y", Operation: "select", Table: "foobar"}, {Input: "SELECT * FROM(SELECT * FROM foobar) WHERE x > y", Operation: "select", Table: "foobar"}, {Input: "SELECT substring('spam' FROM 2 FOR 3) AS x FROM FROMAGE) FROM fromagier", Operation: "select", Table: "fromagier"}, } { tc.test(t) } } func TestParseSQLOther(t *testing.T) { for _, tc := range []sqlTestcase{ // Test that we handle table names enclosed in brackets. {Input: "SELECT * FROM [foo]", Operation: "select", Table: "foo"}, {Input: "SELECT * FROM[foo]", Operation: "select", Table: "foo"}, {Input: "SELECT * FROM [ foo ]", Operation: "select", Table: "foo"}, {Input: "SELECT * FROM [ 'foo' ]", Operation: "select", Table: "foo"}, {Input: "SELECT * FROM[ `something`.'foo' ]", Operation: "select", Table: "foo"}, // Test that we handle the cheese. {Input: "SELECT fromage FROM fromagier", Operation: "select", Table: "fromagier"}, {Input: "SELECT (x from fromage) FROM fromagier", Operation: "select", Table: "fromagier"}, {Input: "SELECT (x FROM FROMAGE) FROM fromagier", Operation: "select", Table: "fromagier"}, {Input: "SELECT substring('spam' FROM 2 FOR 3) AS x FROM FROMAGE) FROM fromagier", Operation: "select", Table: "fromagier"}, } { tc.test(t) } } func TestParseSQLUpdateExtraKeywords(t *testing.T) { for _, tc := range []sqlTestcase{ {Input: "update or rollback foo", Operation: "update", Table: "foo"}, {Input: "update only foo", Operation: "update", Table: "foo"}, {Input: "update low_priority ignore{foo}", Operation: "update", Table: "foo"}, } { tc.test(t) } } func TestLineComment(t *testing.T) { for _, tc := range []sqlTestcase{ { Input: `SELECT -- * FROM tricky * FROM foo`, Operation: "select", Table: "foo", }, { Input: `SELECT # * FROM tricky * FROM foo`, Operation: "select", Table: "foo", }, { Input: ` -- SELECT * FROM tricky SELECT * FROM foo`, Operation: "select", Table: "foo", }, { Input: ` # SELECT * FROM tricky SELECT * FROM foo`, Operation: "select", Table: "foo", }, { Input: `SELECT * FROM -- tricky foo`, Operation: "select", Table: "foo", }, } { tc.test(t) } } func TestSemicolonPrefix(t *testing.T) { for _, tc := range []sqlTestcase{ { Input: `;select * from foo`, Operation: "select", Table: "foo", }, { Input: ` ;; ; select * from foo`, Operation: "select", Table: "foo", }, { Input: ` ; SELECT * FROM foo`, Operation: "select", Table: "foo", }, } { tc.test(t) } } func TestDollarSignTable(t *testing.T) { for _, tc := range []sqlTestcase{ { Input: `select * from $dollar_100_$`, Operation: "select", Table: "$dollar_100_$", }, } { tc.test(t) } } func TestPriorityQuery(t *testing.T) { // Test that we handle: // https://dev.mysql.com/doc/refman/8.0/en/insert.html // INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name for _, tc := range []sqlTestcase{ { Input: `INSERT HIGH_PRIORITY INTO employee VALUES('Tom',12345,'Sales',100)`, Operation: "insert", Table: "employee", }, } { tc.test(t) } } func TestExtractTable(t *testing.T) { for idx, tc := range []string{ "table", "`table`", `"table"`, "`database.table`", "`database`.table", "database.`table`", "`database`.`table`", " { table }", "\n[table]", "\t ( 'database'.`table` ) ", } { table := extractTable(tc) if table != "table" { t.Error(idx, table) } } } go-agent-3.42.0/v3/newrelic/stacktrace.go000066400000000000000000000054561510742411500201260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "path" "runtime" "strings" ) // stackTrace is a stack trace. type stackTrace []uintptr // getStackTrace returns a new stackTrace. func getStackTrace() stackTrace { skip := 1 // skip runtime.Callers callers := make([]uintptr, maxStackTraceFrames) written := runtime.Callers(skip, callers) return callers[:written] } type StacktraceFrame struct { Name string File string Line int64 } func (f StacktraceFrame) formattedName() string { if strings.HasPrefix(f.Name, "go.") { // This indicates an anonymous struct. eg. // "go.(*struct { github.com/newrelic/go-agent.threadWithExtras }).NoticeError" return f.Name } return path.Base(f.Name) } func (f StacktraceFrame) isAgent() bool { // Note this is not a contains conditional rather than a prefix // conditional to handle anonymous functions like: // "go.(*struct { github.com/newrelic/go-agent.threadWithExtras }).NoticeError" return strings.Contains(f.Name, "github.com/newrelic/go-agent/v3/internal.") || strings.Contains(f.Name, "github.com/newrelic/go-agent/v3/newrelic.") } func (f StacktraceFrame) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('{') w := jsonFieldsWriter{buf: buf} if f.Name != "" { w.stringField("name", f.formattedName()) } if f.File != "" { w.stringField("filepath", f.File) } if f.Line != 0 { w.intField("line", f.Line) } buf.WriteByte('}') } func writeFrames(buf *bytes.Buffer, frames []StacktraceFrame) { // Remove top agent frames. for len(frames) > 0 && frames[0].isAgent() { frames = frames[1:] } // Truncate excessively long stack traces (they may be provided by the // customer). if len(frames) > maxStackTraceFrames { frames = frames[0:maxStackTraceFrames] } buf.WriteByte('[') for idx, frame := range frames { if idx > 0 { buf.WriteByte(',') } frame.WriteJSON(buf) } buf.WriteByte(']') } func (st stackTrace) frames() []StacktraceFrame { if len(st) == 0 { return nil } frames := runtime.CallersFrames(st) // CallersFrames is only available in Go 1.7+ fs := make([]StacktraceFrame, 0, maxStackTraceFrames) var frame runtime.Frame more := true for more { frame, more = frames.Next() fs = append(fs, StacktraceFrame{ Name: frame.Function, File: frame.File, Line: int64(frame.Line), }) } return fs } // WriteJSON adds the stack trace to the buffer in the JSON form expected by the // collector. func (st stackTrace) WriteJSON(buf *bytes.Buffer) { frames := st.frames() writeFrames(buf, frames) } // MarshalJSON prepares JSON in the format expected by the collector. func (st stackTrace) MarshalJSON() ([]byte, error) { estimate := 256 * len(st) buf := bytes.NewBuffer(make([]byte, 0, estimate)) st.WriteJSON(buf) return buf.Bytes(), nil } go-agent-3.42.0/v3/newrelic/stacktrace_test.go000066400000000000000000000145301510742411500211560ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" "strings" "testing" "github.com/newrelic/go-agent/v3/internal/stacktracetest" ) func TestGetStackTrace(t *testing.T) { stack := getStackTrace() js, err := json.Marshal(stack) if nil != err { t.Fatal(err) } if nil == js { t.Fatal(string(js)) } } func TestLongStackTraceLimitsFrames(t *testing.T) { st := stacktracetest.CountedCall(maxStackTraceFrames+20, func() []uintptr { return getStackTrace() }) if len(st) != maxStackTraceFrames { t.Error("Unexpected size of stacktrace", maxStackTraceFrames, len(st)) } l := len(stackTrace(st).frames()) if l != maxStackTraceFrames { t.Error("Unexpected number of frames", maxStackTraceFrames, l) } } func TestManyStackTraceFramesLimitsOutput(t *testing.T) { frames := make([]StacktraceFrame, maxStackTraceFrames+20) expect := `[ {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{} ]` estimate := 256 * len(frames) output := bytes.NewBuffer(make([]byte, 0, estimate)) writeFrames(output, frames) if compactJSONString(expect) != output.String() { t.Error("Unexpected JSON output", compactJSONString(expect), output.String()) } } func TestStacktraceFrames(t *testing.T) { // This stacktrace taken from Go 1.13 inputFrames := []StacktraceFrame{ { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/internal/stacktrace.go", Name: "github.com/newrelic/go-agent/v3/internal.GetStackTrace", Line: 18, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/internal_txn.go", Name: "github.com/newrelic/go-agent/v3/newrelic.errDataFromError", Line: 533, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/internal_txn.go", Name: "github.com/newrelic/go-agent/v3/newrelic.(*txn).NoticeError", Line: 575, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/transaction.go", Name: "github.com/newrelic/go-agent/v3/newrelic.(*Transaction).NoticeError", Line: 90, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/examples/server/main.go", Name: "main.noticeError", Line: 30, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.HandlerFunc.ServeHTTP", Line: 2007, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/instrumentation.go", Name: "github.com/newrelic/go-agent/v3/newrelic.WrapHandle.func1", Line: 41, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.HandlerFunc.ServeHTTP", Line: 2007, }, { File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/instrumentation.go", Name: "github.com/newrelic/go-agent/v3/newrelic.WrapHandleFunc.func1", Line: 71, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.HandlerFunc.ServeHTTP", Line: 2007, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.(*ServeMux).ServeHTTP", Line: 2387, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.serverHandler.ServeHTTP", Line: 2802, }, { File: "/Users/will/.gvm/gos/go1.13/src/net/http/server.go", Name: "net/http.(*conn).serve", Line: 1890, }, { File: "/Users/will/.gvm/gos/go1.13/src/runtime/asm_amd64.s", Name: "runtime.goexit", Line: 1357, }, } buf := &bytes.Buffer{} writeFrames(buf, inputFrames) expectedJSON := `[ { "name":"main.noticeError", "filepath":"/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/examples/server/main.go", "line":30 }, { "name":"http.HandlerFunc.ServeHTTP", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":2007 }, { "name":"newrelic.WrapHandle.func1", "filepath":"/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/instrumentation.go", "line":41 }, { "name":"http.HandlerFunc.ServeHTTP", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":2007 }, { "name":"newrelic.WrapHandleFunc.func1", "filepath":"/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/v3/newrelic/instrumentation.go", "line":71 }, { "name":"http.HandlerFunc.ServeHTTP", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":2007 }, { "name":"http.(*ServeMux).ServeHTTP", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":2387 }, { "name":"http.serverHandler.ServeHTTP", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":2802 }, { "name":"http.(*conn).serve", "filepath":"/Users/will/.gvm/gos/go1.13/src/net/http/server.go", "line":1890 }, { "name":"runtime.goexit", "filepath":"/Users/will/.gvm/gos/go1.13/src/runtime/asm_amd64.s", "line":1357 }]` testExpectedJSON(t, expectedJSON, buf.String()) } func TestStackTraceTopFrame(t *testing.T) { // This test uses a separate package since the stacktrace code removes // the top stack frames which are in packages "newrelic" and "internal". stackJSON := stacktracetest.TopStackFrame(func() []byte { st := getStackTrace() js, _ := json.Marshal(st) return js }) stack := []struct { Name string `json:"name"` FilePath string `json:"filepath"` Line int `json:"line"` }{} if err := json.Unmarshal(stackJSON, &stack); err != nil { t.Fatal(err) } if len(stack) < 2 { t.Fatal(string(stackJSON)) } if stack[0].Name != "stacktracetest.TopStackFrame" { t.Error(string(stackJSON)) } if stack[0].Line != 9 { t.Error(string(stackJSON)) } if !strings.Contains(stack[0].FilePath, "go-agent/v3/internal/stacktracetest/stacktracetest.go") { t.Error(string(stackJSON)) } } func TestFramesCount(t *testing.T) { st := stacktracetest.CountedCall(3, func() []uintptr { return getStackTrace() }) frames := stackTrace(st).frames() if len(st) != len(frames) { t.Error("Invalid # of frames", len(st), len(frames)) } } go-agent-3.42.0/v3/newrelic/synthetics_test.go000066400000000000000000000167751510742411500212440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "net/http" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" "github.com/newrelic/go-agent/v3/internal/crossagent" ) type harvestedTxnTrace struct { startTimeMs float64 durationToResponse float64 transactionName string requestURL string traceDetails struct { attributes struct { agentAttributes eventAttributes userAttributes eventAttributes intrinsics eventAttributes } } catGUID string forcePersistFlag bool xraySessionID string syntheticsResourceID string } func (h *harvestedTxnTrace) UnmarshalJSON(data []byte) error { var arr []interface{} if err := json.Unmarshal(data, &arr); err != nil { return err } if len(arr) != 10 { return fmt.Errorf("unexpected number of transaction trace items: %d", len(arr)) } h.startTimeMs = arr[0].(float64) h.durationToResponse = arr[1].(float64) h.transactionName = arr[2].(string) if nil != arr[3] { h.requestURL = arr[3].(string) } // Item 4 -- the trace -- will be dealt with shortly. h.catGUID = arr[5].(string) // Item 6 intentionally ignored. h.forcePersistFlag = arr[7].(bool) if arr[8] != nil { h.xraySessionID = arr[8].(string) } h.syntheticsResourceID = arr[9].(string) traceDetails := arr[4].([]interface{}) attributes := traceDetails[4].(map[string]interface{}) h.traceDetails.attributes.agentAttributes = attributes["agentAttributes"].(map[string]interface{}) h.traceDetails.attributes.userAttributes = attributes["userAttributes"].(map[string]interface{}) h.traceDetails.attributes.intrinsics = attributes["intrinsics"].(map[string]interface{}) return nil } func harvestTxnDataTrace(t *txnData) (*harvestedTxnTrace, error) { // Since transaction trace JSON is built using string manipulation, we have // to do an awkward marshal/unmarshal shuffle to be able to verify the // intrinsics. ht := harvestTrace{ txnEvent: t.txnEvent, Trace: t.TxnTrace, } js, err := ht.MarshalJSON() if err != nil { return nil, err } trace := &harvestedTxnTrace{} if err := json.Unmarshal(js, trace); err != nil { return nil, err } return trace, nil } func TestSynthetics(t *testing.T) { var testcases []struct { Name string `json:"name"` Settings struct { AgentEncodingKey string `json:"agentEncodingKey"` SyntheticsEncodingKey string `json:"syntheticsEncodingKey"` TransactionGUID string `json:"transactionGuid"` TrustedAccountIDs []int `json:"trustedAccountIds"` } `json:"settings"` InputHeaderPayload json.RawMessage `json:"inputHeaderPayload"` InputObfuscatedHeader map[string]string `json:"inputObfuscatedHeader"` OutputTransactionTrace struct { Header struct { Field9 string `json:"field_9"` } `json:"header"` ExpectedIntrinsics map[string]string `json:"expectedIntrinsics"` NonExpectedIntrinsics []string `json:"nonExpectedIntrinsics"` } `json:"outputTransactionTrace"` OutputTransactionEvent struct { ExpectedAttributes map[string]string `json:"expectedAttributes"` NonExpectedAttributes []string `json:"nonExpectedAttributes"` } `json:"outputTransactionEvent"` OutputExternalRequestHeader struct { ExpectedHeader map[string]string `json:"expectedHeader"` NonExpectedHeader []string `json:"nonExpectedHeader"` } `json:"outputExternalRequestHeader"` } err := crossagent.ReadJSON("synthetics/synthetics.json", &testcases) if err != nil { t.Fatal(err) } for _, tc := range testcases { // Fake enough transaction data to run the test. tr := &txnData{ Name: "txn", } tr.CrossProcess.Init(false, false, &internal.ConnectReply{ CrossProcessID: "1#1", TrustedAccounts: make(map[int]struct{}), EncodingKey: tc.Settings.AgentEncodingKey, }) // Set up the trusted accounts. for _, account := range tc.Settings.TrustedAccountIDs { tr.CrossProcess.TrustedAccounts[account] = struct{}{} } // Set up the GUID. if tc.Settings.TransactionGUID != "" { tr.CrossProcess.GUID = tc.Settings.TransactionGUID } // Parse the input header, ignoring any errors. inputHeaders := make(http.Header) for k, v := range tc.InputObfuscatedHeader { inputHeaders.Add(k, v) } tr.CrossProcess.handleInboundRequestEncodedSynthetics(inputHeaders.Get(cat.NewRelicSyntheticsName)) // Get the headers for an external request. metadata, err := tr.CrossProcess.CreateCrossProcessMetadata("txn", "app") if err != nil { t.Fatalf("%s: error creating outbound request headers: %v", tc.Name, err) } // Verify that the header either exists or doesn't exist, depending on the // test case. headers := metadataToHTTPHeader(metadata) for key, value := range tc.OutputExternalRequestHeader.ExpectedHeader { obfuscated := headers.Get(key) if obfuscated == "" { t.Errorf("%s: expected output header %s not found", tc.Name, key) } else if value != obfuscated { t.Errorf("%s: expected output header %s mismatch: expected=%s; got=%s", tc.Name, key, value, obfuscated) } } for _, key := range tc.OutputExternalRequestHeader.NonExpectedHeader { if value := headers.Get(key); value != "" { t.Errorf("%s: output header %s expected to be missing; got %s", tc.Name, key, value) } } // Harvest the trace. trace, err := harvestTxnDataTrace(tr) if err != nil { t.Errorf("%s: error harvesting trace data: %v", tc.Name, err) } // Check the synthetics resource ID. if trace.syntheticsResourceID != tc.OutputTransactionTrace.Header.Field9 { t.Errorf("%s: unexpected field 9: expected=%s; got=%s", tc.Name, tc.OutputTransactionTrace.Header.Field9, trace.syntheticsResourceID) } // Check for expected intrinsics. for key, value := range tc.OutputTransactionTrace.ExpectedIntrinsics { // First, check if the key exists at all. if !trace.traceDetails.attributes.intrinsics.has(key) { t.Fatalf("%s: missing intrinsic %s", tc.Name, key) } // Everything we're looking for is a string, so we can be a little lazy // here. if err := trace.traceDetails.attributes.intrinsics.isString(key, value); err != nil { t.Errorf("%s: %v", tc.Name, err) } } // Now we verify that the unexpected intrinsics didn't miraculously appear. for _, key := range tc.OutputTransactionTrace.NonExpectedIntrinsics { if trace.traceDetails.attributes.intrinsics.has(key) { t.Errorf("%s: expected intrinsic %s to be missing; instead, got value %v", tc.Name, key, trace.traceDetails.attributes.intrinsics[key]) } } // Harvest the event. event, err := harvestTxnDataEvent(tr) if err != nil { t.Errorf("%s: error harvesting event data: %v", tc.Name, err) } // Now we have the event, let's look for the expected intrinsics. for key, value := range tc.OutputTransactionEvent.ExpectedAttributes { // First, check if the key exists at all. if !event.intrinsics.has(key) { t.Fatalf("%s: missing intrinsic %s", tc.Name, key) } // Everything we're looking for is a string, so we can be a little lazy // here. if err := event.intrinsics.isString(key, value); err != nil { t.Errorf("%s: %v", tc.Name, err) } } // Now we verify that the unexpected intrinsics didn't miraculously appear. for _, key := range tc.OutputTransactionEvent.NonExpectedAttributes { if event.intrinsics.has(key) { t.Errorf("%s: expected intrinsic %s to be missing; instead, got value %v", tc.Name, key, event.intrinsics[key]) } } } } go-agent-3.42.0/v3/newrelic/trace_context_test.go000066400000000000000000000251421510742411500216750ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "reflect" "strings" "testing" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/crossagent" ) type fieldExpect struct { Exact map[string]interface{} `json:"exact,omitempty"` Expected []string `json:"expected,omitempty"` Unexpected []string `json:"unexpected,omitempty"` NotEqual map[string]interface{} `json:"notequal,omitempty"` Vendors []string `json:"vendors,omitempty"` } type TraceContextTestCase struct { TestName string `json:"test_name"` TrustedAccountKey string `json:"trusted_account_key"` AccountID string `json:"account_id"` WebTransaction bool `json:"web_transaction"` RaisesException bool `json:"raises_exception"` ForceSampledTrue bool `json:"force_sampled_true"` SpanEventsEnabled bool `json:"span_events_enabled"` TxnEventsEnabled bool `json:"transaction_events_enabled"` TransportType string `json:"transport_type"` InboundHeaders []map[string]string `json:"inbound_headers"` OutboundPayloads []fieldExpect `json:"outbound_payloads,omitempty"` ExpectedMetrics [][2]interface{} `json:"expected_metrics"` Intrinsics struct { TargetEvents []string `json:"target_events"` Common *fieldExpect `json:"common,omitempty"` Transaction *fieldExpect `json:"Transaction,omitempty"` Span *fieldExpect `json:"Span,omitempty"` TransactionError *fieldExpect `json:"TransactionError,omitempty"` } `json:"intrinsics"` } func TestJSONDTHeaders(t *testing.T) { type testcase struct { in string out http.Header err bool } for i, test := range []testcase{ {"", http.Header{}, false}, {"{}", http.Header{}, false}, {" invalid ", http.Header{}, true}, {`"foo"`, http.Header{}, true}, {`{"foo": "bar"}`, http.Header{ "Foo": {"bar"}, }, false}, {`{ "foo": "bar", "baz": "quux", "multiple": [ "alpha", "beta", "gamma" ] }`, http.Header{ "Foo": {"bar"}, "Baz": {"quux"}, "Multiple": {"alpha", "beta", "gamma"}, }, false}, } { h, err := DistributedTraceHeadersFromJSON(test.in) if err != nil { if !test.err { t.Errorf("case %d: %v: error expected but not generated", i, test.in) } } else if !reflect.DeepEqual(test.out, h) { t.Errorf("case %d, %v -> %v but expected %v", i, test.in, h, test.out) } } } func TestCrossAgentW3CTraceContext(t *testing.T) { var tcs []TraceContextTestCase data, err := crossagent.ReadFile("distributed_tracing/trace_context.json") if err != nil { t.Fatal(err) } if err := json.Unmarshal(data, &tcs); nil != err { t.Fatal(err) } for _, tc := range tcs { t.Run(tc.TestName, func(t *testing.T) { if tc.TestName == "spans_disabled_in_child" || tc.TestName == "spans_disabled_root" { t.Skip("spec change caused failing test, skipping") return } runW3CTestCase(t, tc) }) } } func runW3CTestCase(t *testing.T, tc TraceContextTestCase) { configCallback := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = true cfg.SpanEvents.Enabled = tc.SpanEventsEnabled cfg.TransactionEvents.Enabled = tc.TxnEventsEnabled } app := testApp(func(reply *internal.ConnectReply) { reply.AccountID = tc.AccountID reply.AppID = "456" reply.PrimaryAppID = "456" reply.TrustedAccountKey = tc.TrustedAccountKey reply.SetSampleEverything() }, configCallback, t) txn := app.StartTransaction("hello") if tc.WebTransaction { txn.SetWebRequestHTTP(nil) } // If the tests wants us to have an error, give 'em an error if tc.RaisesException { txn.NoticeError(errors.New("my error message")) } // If there are no inbound payloads, invoke Accept on an empty inbound payload. if nil == tc.InboundHeaders { txn.AcceptDistributedTraceHeaders(getTransportType(tc.TransportType), nil) } txn.AcceptDistributedTraceHeaders(getTransportType(tc.TransportType), headersFromStringMap(tc.InboundHeaders)) // Call create each time an outbound payload appears in the testcase for _, expect := range tc.OutboundPayloads { hdrs := http.Header{} txn.InsertDistributedTraceHeaders(hdrs) assertTestCaseOutboundHeaders(expect, t, hdrs) } txn.End() // create WantMetrics and assert var wantMetrics []internal.WantMetric for _, metric := range tc.ExpectedMetrics { wantMetrics = append(wantMetrics, internal.WantMetric{Name: metric[0].(string), Scope: "", Forced: nil, Data: nil}) } app.ExpectMetricsPresent(t, wantMetrics) // Add extra fields that are not listed in the JSON file so that we can // always do exact intrinsic set match. extraTxnFields := &fieldExpect{Expected: []string{"name"}} if tc.WebTransaction { extraTxnFields.Expected = append(extraTxnFields.Expected, "nr.apdexPerfZone") } extraSpanFields := &fieldExpect{ Expected: []string{"name", "transaction.name", "category", "nr.entryPoint"}, } // There is a single test with an error (named "exception"), so these // error expectations can be hard coded. extraErrorFields := &fieldExpect{ Expected: []string{"parent.type", "parent.account", "parent.app", "parent.transportType", "error.message", "transactionName", "parent.transportDuration", "error.class", "spanId"}, } for _, value := range tc.Intrinsics.TargetEvents { switch value { case "Transaction": assertW3CTestCaseIntrinsics(t, app.ExpectTxnEvents, tc.Intrinsics.Common, tc.Intrinsics.Transaction, extraTxnFields) case "Span": assertW3CTestCaseIntrinsics(t, app.ExpectSpanEvents, tc.Intrinsics.Common, tc.Intrinsics.Span, extraSpanFields) case "TransactionError": assertW3CTestCaseIntrinsics(t, app.ExpectErrorEvents, tc.Intrinsics.Common, tc.Intrinsics.TransactionError, extraErrorFields) } } } // getTransport ensures that our transport names match cross agent test values. func getTransportType(transport string) TransportType { switch TransportType(transport) { case TransportHTTP, TransportHTTPS, TransportKafka, TransportJMS, TransportIronMQ, TransportAMQP, TransportQueue, TransportOther: return TransportType(transport) default: return TransportUnknown } } func headersFromStringMap(hdrs []map[string]string) http.Header { httpHdrs := http.Header{} for _, entry := range hdrs { for k, v := range entry { httpHdrs.Add(k, v) } } return httpHdrs } func assertTestCaseOutboundHeaders(expect fieldExpect, t *testing.T, hdrs http.Header) { p := make(map[string]string) // prepare traceparent header pHdr := hdrs.Get("traceparent") pSplit := strings.Split(pHdr, "-") if len(pSplit) != 4 { t.Error("incorrect traceparent header created ", pHdr) return } p["traceparent.version"] = pSplit[0] p["traceparent.trace_id"] = pSplit[1] p["traceparent.parent_id"] = pSplit[2] p["traceparent.trace_flags"] = pSplit[3] // prepare tracestate header sHdr := hdrs.Get("tracestate") sSplit := strings.Split(sHdr, "-") if len(sSplit) >= 9 { p["tracestate.tenant_id"] = strings.Split(sHdr, "@")[0] p["tracestate.version"] = strings.Split(sSplit[0], "=")[1] p["tracestate.parent_type"] = sSplit[1] p["tracestate.parent_account_id"] = sSplit[2] p["tracestate.parent_application_id"] = sSplit[3] p["tracestate.span_id"] = sSplit[4] p["tracestate.transaction_id"] = sSplit[5] p["tracestate.sampled"] = sSplit[6] p["tracestate.priority"] = sSplit[7] p["tracestate.timestamp"] = sSplit[8] } // prepare newrelic header nHdr := hdrs.Get("newrelic") decoded, err := base64.StdEncoding.DecodeString(nHdr) if err != nil { t.Error("failure to decode newrelic header: ", err) } nrPayload := struct { Version [2]int `json:"v"` Data payload `json:"d"` }{} if err := json.Unmarshal(decoded, &nrPayload); nil != err { t.Error("unable to unmarshall newrelic header: ", err) } p["newrelic.v"] = fmt.Sprintf("%v", nrPayload.Version) p["newrelic.d.ac"] = nrPayload.Data.Account p["newrelic.d.ap"] = nrPayload.Data.App p["newrelic.d.id"] = nrPayload.Data.ID p["newrelic.d.pr"] = fmt.Sprintf("%v", nrPayload.Data.Priority) p["newrelic.d.ti"] = fmt.Sprintf("%v", nrPayload.Data.Timestamp) p["newrelic.d.tr"] = nrPayload.Data.TracedID p["newrelic.d.tx"] = nrPayload.Data.TransactionID p["newrelic.d.ty"] = nrPayload.Data.Type if *nrPayload.Data.Sampled { p["newrelic.d.sa"] = "1" } else { p["newrelic.d.sa"] = "0" } // Affirm that the exact values are in the payload. for k, v := range expect.Exact { var exp string switch val := v.(type) { case bool: if val { exp = "1" } else { exp = "0" } case string: exp = val default: exp = fmt.Sprintf("%v", val) } if val := p[k]; val != exp { t.Errorf("expected outbound payload wrong value for key %s, expected=%s, actual=%s", k, exp, val) } } // Affirm that the expected values are in the actual payload. for _, e := range expect.Expected { if val := p[e]; val == "" { t.Errorf("expected outbound payload missing key %s", e) } } // Affirm that the unexpected values are not in the actual payload. for _, e := range expect.Unexpected { if val := p[e]; val != "" { t.Errorf("expected outbound payload contains key %s", e) } } // Affirm that not equal values are not equal in the actual payload for k, v := range expect.NotEqual { exp := fmt.Sprintf("%v", v) if val := p[k]; val == exp { t.Errorf("expected outbound payload has equal value for key %s, value=%s", k, val) } } // Affirm that the correct vendors are included in the actual payload for _, e := range expect.Vendors { if !strings.Contains(sHdr, e) { t.Errorf("expected outbound payload does not contain vendor %s, tracestate=%s", e, sHdr) } } if sHdr != "" { // when the tracestate header is non-empty, ensure that no extraneous // vendors appear if cnt := strings.Count(sHdr, "="); cnt != len(expect.Vendors)+1 { t.Errorf("expected outbound payload has wrong number of vendors, tracestate=%s", sHdr) } } } func assertW3CTestCaseIntrinsics(t internal.Validator, expect func(internal.Validator, []internal.WantEvent), fields ...*fieldExpect) { intrinsics := map[string]interface{}{} for _, f := range fields { f.add(intrinsics) } expect(t, []internal.WantEvent{{Intrinsics: intrinsics}}) } func (fe *fieldExpect) add(intrinsics map[string]interface{}) { if nil != fe { for k, v := range fe.Exact { intrinsics[k] = v } for _, v := range fe.Expected { intrinsics[v] = internal.MatchAnything } } } go-agent-3.42.0/v3/newrelic/trace_observer.go000066400000000000000000000475151510742411500210110ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "context" "crypto/tls" "errors" "io" "strings" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/backoff" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "github.com/newrelic/go-agent/v3/internal" v1 "github.com/newrelic/go-agent/v3/internal/com_newrelic_trace_v1" ) type gRPCtraceObserver struct { initialConnSuccess chan struct{} // initConnOnce protects initialConnSuccess from being closed multiple times. initConnOnce sync.Once initiateShutdown chan struct{} // initShutdownOnce protects initiateShutdown from being closed multiple times. initShutdownOnce sync.Once messages chan *spanEvent restartChan chan struct{} shutdownComplete chan struct{} metadata metadata.MD metadataLock sync.Mutex // dialOptions are the grpc.DialOptions to be used when calling grpc.Dial. dialOptions []grpc.DialOption supportability *observerSupport observerConfig } type observerSupport struct { increment chan string dump chan map[string]float64 } const ( // versionSupports8T records whether we are using a supported version of Go // for Infinite Tracing versionSupports8T = true grpcVersion = grpc.Version // recordSpanBackoff is the time to wait after a failure on the RecordSpan // endpoint before retrying recordSpanBackoff = 15 * time.Second // numCodes is the total number of grpc.Codes numCodes = 17 licenseMetadataKey = "license_key" runIDMetadataKey = "agent_run_token" observerSeen = "Supportability/InfiniteTracing/Span/Seen" observerSent = "Supportability/InfiniteTracing/Span/Sent" observerCodeErr = "Supportability/InfiniteTracing/Span/gRPC/" observerResponseErr = "Supportability/InfiniteTracing/Span/Response/Error" ) var ( codeStrings = map[codes.Code]string{ codes.Code(0): "OK", codes.Code(1): "CANCELLED", codes.Code(2): "UNKNOWN", codes.Code(3): "INVALID_ARGUMENT", codes.Code(4): "DEADLINE_EXCEEDED", codes.Code(5): "NOT_FOUND", codes.Code(6): "ALREADY_EXISTS", codes.Code(7): "PERMISSION_DENIED", codes.Code(8): "RESOURCE_EXHAUSTED", codes.Code(9): "FAILED_PRECONDITION", codes.Code(10): "ABORTED", codes.Code(11): "OUT_OF_RANGE", codes.Code(12): "UNIMPLEMENTED", codes.Code(13): "INTERNAL", codes.Code(14): "UNAVAILABLE", codes.Code(15): "DATA_LOSS", codes.Code(16): "UNAUTHENTICATED", } ) type obsResult struct { // shutdown is if the trace observer should shutdown and stop sending // spans. shutdown bool // backoff is true if a backoff should be used before reconnecting to // RecordSpan. backoff bool } func newTraceObserver(runID internal.AgentRunID, requestHeadersMap map[string]string, cfg observerConfig) (traceObserver, error) { to := &gRPCtraceObserver{ messages: make(chan *spanEvent, cfg.queueSize), initialConnSuccess: make(chan struct{}), restartChan: make(chan struct{}, 1), initiateShutdown: make(chan struct{}), shutdownComplete: make(chan struct{}), metadata: newMetadata(runID, cfg.license, requestHeadersMap), observerConfig: cfg, supportability: newObserverSupport(), dialOptions: newDialOptions(cfg), } go to.handleSupportability() go func() { to.connectToTraceObserver() // Closing shutdownComplete must be done before draining the messages. // This prevents spans from being put onto the messages channel while // we are trying to empty the channel. close(to.shutdownComplete) for len(to.messages) > 0 { // drain the channel <-to.messages } }() return to, nil } // newMetadata creates a grpc metadata with proper keys and values for use when // connecting to RecordSpan. func newMetadata(runID internal.AgentRunID, license string, requestHeadersMap map[string]string) metadata.MD { md := metadata.New(requestHeadersMap) md.Set(licenseMetadataKey, license) md.Set(runIDMetadataKey, string(runID)) return md } // markInitialConnSuccessful closes the gRPCtraceObserver initialConnSuccess channel and // is safe to call multiple times. func (to *gRPCtraceObserver) markInitialConnSuccessful() { to.initConnOnce.Do(func() { close(to.initialConnSuccess) }) } // startShutdown closes the gRPCtraceObserver initiateShutdown channel and // is safe to call multiple times. func (to *gRPCtraceObserver) startShutdown() { to.initShutdownOnce.Do(func() { close(to.initiateShutdown) }) } func newDialOptions(cfg observerConfig) []grpc.DialOption { do := []grpc.DialOption{ grpc.WithConnectParams(grpc.ConnectParams{ Backoff: backoff.Config{ BaseDelay: 15 * time.Second, Multiplier: 2, MaxDelay: 300 * time.Second, }, }), } if cfg.endpoint.secure { do = append(do, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) } else { do = append(do, grpc.WithInsecure()) } if nil != cfg.dialer { do = append(do, grpc.WithContextDialer(cfg.dialer)) } return do } func (to *gRPCtraceObserver) connectToTraceObserver() { conn, err := grpc.Dial(to.endpoint.host, to.dialOptions...) if nil != err { errMsg := strings.Replace(err.Error(), to.license, "--REDACTED_LICENSE_KEY--", -1) // this error is unrecoverable and will not be retried to.log.Error("trace observer unable to dial grpc endpoint", map[string]interface{}{ "host": to.endpoint.host, "err": errMsg, }) return } defer to.closeConn(conn) serviceClient := v1.NewIngestServiceClient(conn) for { result := to.connectToStream(serviceClient) if result.shutdown { return } if result.backoff && !to.removeBackoff { time.Sleep(recordSpanBackoff) } } } func (to *gRPCtraceObserver) closeConn(conn *grpc.ClientConn) { // Related to https://github.com/grpc/grpc-go/issues/2159 // If we call conn.Close() immediately, some messages may still be // buffered and will never be sent. Initial testing suggests this takes // around 150-200ms with a full channel. time.Sleep(500 * time.Millisecond) if err := conn.Close(); nil != err { to.log.Info("closing trace observer connection was not successful", map[string]interface{}{ "err": err.Error(), }) } } func (to *gRPCtraceObserver) connectToStream(serviceClient v1.IngestServiceClient) obsResult { to.metadataLock.Lock() md := to.metadata to.metadataLock.Unlock() ctx := metadata.NewOutgoingContext(context.Background(), md) spanClient, err := serviceClient.RecordSpan(ctx) if nil != err { to.log.Error("trace observer unable to create span client", map[string]interface{}{ "err": err.Error(), }) return obsResult{ shutdown: false, backoff: true, } } defer to.closeSpanClient(spanClient) to.markInitialConnSuccessful() responseError := make(chan error, 1) go to.rcvResponses(spanClient, responseError) for { select { case msg := <-to.messages: result, success := to.trySendSpan(spanClient, msg, responseError) if !success { return result } case <-to.restartChan: return obsResult{ shutdown: false, backoff: false, } case err := <-responseError: return obsResult{ shutdown: errShouldShutdown(err), backoff: errShouldBackoff(err), } case <-to.initiateShutdown: to.drainQueue(spanClient) return obsResult{ shutdown: true, backoff: false, } } } } func (to *gRPCtraceObserver) rcvResponses(spanClient v1.IngestService_RecordSpanClient, responseError chan error) { for { s, err := spanClient.Recv() if nil != err { // (issue 213) These two specific errors were reported as nuisance // but are really harmless so we'll report them as DEBUG level events // instead of ERROR. // This error comes from our Infinite Tracing load balancers. // We believe the EOF error comes from the gRPC getting reset every 30 seconds // from the same cause (rebalancing 8T) if err.Error() == "rpc error: code = Internal desc = stream terminated by RST_STREAM with error code: NO_ERROR" || err.Error() == "EOF" { to.log.Debug("trace observer response error", map[string]interface{}{ "err": err.Error(), }) } else { to.log.Error("trace observer response error", map[string]interface{}{ "err": err.Error(), }) } // NOTE: even when the trace observer is shutting down // properly, an EOF error will be received here and a // supportability metric created. to.supportabilityError(err) responseError <- err return } to.log.Debug("trace observer response", map[string]interface{}{ "messages_seen": s.GetMessagesSeen(), }) } } func (to *gRPCtraceObserver) drainQueue(spanClient v1.IngestService_RecordSpanClient) { numSpans := len(to.messages) for i := 0; i < numSpans; i++ { msg := <-to.messages if err := to.sendSpan(spanClient, msg); err != nil { // if we fail to send a span, do not send the rest break } } } func (to *gRPCtraceObserver) trySendSpan(spanClient v1.IngestService_RecordSpanClient, msg *spanEvent, responseError chan error) (obsResult, bool) { if sendErr := to.sendSpan(spanClient, msg); sendErr != nil { // When send closes so does recv. Check the error on recv // because it could be a shutdown request when the error from // send was not. var respErr error ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() select { case respErr = <-responseError: case <-ticker.C: to.log.Debug("timeout waiting for response error from trace observer", nil) } return obsResult{ shutdown: errShouldShutdown(sendErr) || errShouldShutdown(respErr), backoff: errShouldBackoff(sendErr) || errShouldBackoff(respErr), }, false } return obsResult{}, true } func (to *gRPCtraceObserver) closeSpanClient(spanClient v1.IngestService_RecordSpanClient) { to.log.Debug("closing trace observer sender", map[string]interface{}{}) if err := spanClient.CloseSend(); err != nil { to.log.Debug("error closing trace observer sender", map[string]interface{}{ "err": err.Error(), }) } } // restart enqueues a request to restart with a new run ID func (to *gRPCtraceObserver) restart(runID internal.AgentRunID, requestHeadersMap map[string]string) { to.metadataLock.Lock() to.metadata = newMetadata(runID, to.license, requestHeadersMap) to.metadataLock.Unlock() // If there is already a restart on the channel, we don't need to add another select { case to.restartChan <- struct{}{}: default: } } var errTimeout = errors.New("timeout exceeded while waiting for trace observer shutdown to complete") // shutdown initiates a shutdown of the trace observer and blocks until either // shutdown is complete (including draining existing spans from the messages channel) // or the given timeout is hit. func (to *gRPCtraceObserver) shutdown(timeout time.Duration) error { to.startShutdown() ticker := time.NewTicker(timeout) defer ticker.Stop() // Block until the observer shutdown is complete or timeout hit select { case <-to.shutdownComplete: return nil case <-ticker.C: return errTimeout } } // initialConnCompleted indicates that the initial connection to the remote trace // observer was made, but it does NOT indicate anything about the current state of the // connection func (to *gRPCtraceObserver) initialConnCompleted() bool { select { case <-to.initialConnSuccess: return true default: return false } } // errShouldShutdown returns true if the given error is an Unimplemented error // meaning the connection to the trace observer should be shutdown. func errShouldShutdown(err error) bool { return status.Code(err) == codes.Unimplemented } // errShouldBackoff returns true if the given error should cause the trace // observer to retry the connection after a backoff period. func errShouldBackoff(err error) bool { return status.Code(err) != codes.OK && err != io.EOF } func (to *gRPCtraceObserver) sendSpan(spanClient v1.IngestService_RecordSpanClient, msg *spanEvent) error { span := transformEvent(msg) to.supportability.increment <- observerSent if err := spanClient.Send(span); err != nil { to.log.Error("trace observer send error", map[string]interface{}{ "err": err.Error(), }) to.supportabilityError(err) return err } return nil } func (to *gRPCtraceObserver) handleSupportability() { metrics := newSupportMetrics() for { select { case <-to.appShutdown: // Only close this goroutine once the application _and_ the trace // observer have shutdown. This is because we will want to continue // to increment the Seen/Sent metrics when the application is // running but the trace observer is not. return case key := <-to.supportability.increment: metrics[key]++ case to.supportability.dump <- metrics: // reset the metrics map metrics = newSupportMetrics() } } } func newSupportMetrics() map[string]float64 { // grpc codes, plus 2 for seen/sent, plus 1 for response errs metrics := make(map[string]float64, numCodes+3) // these two metrics must always be sent metrics[observerSeen] = 0 metrics[observerSent] = 0 return metrics } func newObserverSupport() *observerSupport { return &observerSupport{ increment: make(chan string), dump: make(chan map[string]float64), } } // dumpSupportabilityMetrics reads the current supportability metrics off of // the channel and resets them to 0. func (to *gRPCtraceObserver) dumpSupportabilityMetrics() map[string]float64 { if to.isAppShutdownComplete() { return nil } return <-to.supportability.dump } func errToCodeString(err error) string { code := status.Code(err) str, ok := codeStrings[code] if !ok { str = strings.ToUpper(code.String()) } return str } func (to *gRPCtraceObserver) supportabilityError(err error) { to.supportability.increment <- observerCodeErr + errToCodeString(err) to.supportability.increment <- observerResponseErr } func obsvString(s string) *v1.AttributeValue { return &v1.AttributeValue{Value: &v1.AttributeValue_StringValue{StringValue: s}} } func obsvBool(b bool) *v1.AttributeValue { return &v1.AttributeValue{Value: &v1.AttributeValue_BoolValue{BoolValue: b}} } func obsvInt(x int64) *v1.AttributeValue { return &v1.AttributeValue{Value: &v1.AttributeValue_IntValue{IntValue: x}} } func obsvDouble(x float64) *v1.AttributeValue { return &v1.AttributeValue{Value: &v1.AttributeValue_DoubleValue{DoubleValue: x}} } func transformEvent(e *spanEvent) *v1.Span { span := &v1.Span{ TraceId: e.TraceID, Intrinsics: make(map[string]*v1.AttributeValue), UserAttributes: make(map[string]*v1.AttributeValue), AgentAttributes: make(map[string]*v1.AttributeValue), } span.Intrinsics["type"] = obsvString("Span") span.Intrinsics["traceId"] = obsvString(e.TraceID) span.Intrinsics["guid"] = obsvString(e.GUID) if "" != e.ParentID { span.Intrinsics["parentId"] = obsvString(e.ParentID) } span.Intrinsics["transactionId"] = obsvString(e.TransactionID) span.Intrinsics["sampled"] = obsvBool(e.Sampled) span.Intrinsics["priority"] = obsvDouble(float64(e.Priority.Float32())) span.Intrinsics["timestamp"] = obsvInt(e.Timestamp.UnixNano() / (1000 * 1000)) // in milliseconds span.Intrinsics["duration"] = obsvDouble(e.Duration.Seconds()) span.Intrinsics["name"] = obsvString(e.Name) span.Intrinsics["category"] = obsvString(string(e.Category)) if e.IsEntrypoint { span.Intrinsics["nr.entryPoint"] = obsvBool(true) } if e.Component != "" { span.Intrinsics["component"] = obsvString(e.Component) } if e.Kind != "" { span.Intrinsics["span.kind"] = obsvString(e.Kind) } if "" != e.TrustedParentID { span.Intrinsics["trustedParentId"] = obsvString(e.TrustedParentID) } if "" != e.TracingVendors { span.Intrinsics["tracingVendors"] = obsvString(e.TracingVendors) } if "" != e.TxnName { span.Intrinsics["transaction.name"] = obsvString(e.TxnName) } copyAttrs(e.AgentAttributes, span.AgentAttributes) copyAttrs(e.UserAttributes, span.UserAttributes) return span } func copyAttrs(source spanAttributeMap, dest map[string]*v1.AttributeValue) { for key, val := range source { switch v := val.(type) { case stringJSONWriter: dest[key] = obsvString(string(v)) case intJSONWriter: dest[key] = obsvInt(int64(v)) case boolJSONWriter: dest[key] = obsvBool(bool(v)) case floatJSONWriter: dest[key] = obsvDouble(float64(v)) default: b := bytes.Buffer{} val.WriteJSON(&b) s := strings.Trim(b.String(), `"`) dest[key] = obsvString(s) } } } // consumeSpan enqueues the span to be sent to the remote trace observer func (to *gRPCtraceObserver) consumeSpan(span *spanEvent) { if to.isAppShutdownComplete() { return } to.supportability.increment <- observerSeen if to.isShutdownInitiated() { return } select { case to.messages <- span: default: if to.log.DebugEnabled() { to.log.Debug("could not send span to trace observer because channel is full", map[string]interface{}{ "channel size": to.queueSize, }) } } return } // isShutdownComplete returns a bool if the trace observer has been shutdown. func (to *gRPCtraceObserver) isShutdownComplete() bool { return isChanClosed(to.shutdownComplete) } // isShutdownInitiated returns a bool if the trace observer has started // shutting down. func (to *gRPCtraceObserver) isShutdownInitiated() bool { return isChanClosed(to.initiateShutdown) } // isAppShutdownComplete returns a bool if the trace observer's application has // been shutdown. func (to *gRPCtraceObserver) isAppShutdownComplete() bool { return isChanClosed(to.appShutdown) } func isChanClosed(c chan struct{}) bool { select { case <-c: return true default: } return false } // The following functions are only used in testing, but are required during compile time in // expect_implementation.go, so they are included here rather than in trace_observer_impl_test.go func expectObserverEvents(v internal.Validator, events *analyticsEvents, expect []internal.WantEvent, extraAttributes map[string]interface{}) { for i, e := range expect { if nil != e.Intrinsics { e.Intrinsics = mergeAttributes(extraAttributes, e.Intrinsics) } event := events.events[i].jsonWriter.(*spanEvent) expectObserverEvent(v, event, e) } } func expectObserverEvent(v internal.Validator, e *spanEvent, expect internal.WantEvent) { span := transformEvent(e) if nil != expect.Intrinsics { expectObserverAttributes(v, span.Intrinsics, expect.Intrinsics) } if nil != expect.UserAttributes { expectObserverAttributes(v, span.UserAttributes, expect.UserAttributes) } if nil != expect.AgentAttributes { expectObserverAttributes(v, span.AgentAttributes, expect.AgentAttributes) } } func expectObserverAttributes(v internal.Validator, actual map[string]*v1.AttributeValue, expect map[string]interface{}) { if len(actual) != len(expect) { v.Error("attributes length difference in trace observer. actual:", len(actual), "expect:", len(expect)) } for key, val := range expect { found, ok := actual[key] if !ok { v.Error("expected attribute not found in trace observer: ", key) continue } if val == internal.MatchAnything { continue } switch exp := val.(type) { case bool: if f := found.GetBoolValue(); f != exp { v.Error("incorrect bool value for key", key, "in trace observer. actual:", f, "expect:", exp) } case string: if f := found.GetStringValue(); f != exp { v.Error("incorrect string value for key", key, "in trace observer. actual:", f, "expect:", exp) } case float64: plusOrMinus := 0.0000001 // with floating point math we can only get so close if f := found.GetDoubleValue(); f-exp > plusOrMinus || exp-f > plusOrMinus { v.Error("incorrect double value for key", key, "in trace observer. actual:", f, "expect:", exp) } case int: if f := found.GetIntValue(); f != int64(exp) { v.Error("incorrect int value for key", key, "in trace observer. actual:", f, "expect:", exp) } default: v.Error("unknown type for key", key, "in trace observer. expected:", exp) } } for key, val := range actual { _, ok := expect[key] if !ok { v.Error("unexpected attribute present in trace observer. key:", key, "value:", val) continue } } } go-agent-3.42.0/v3/newrelic/trace_observer_1_8.go000066400000000000000000000013001510742411500214360ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !go1.9 // +build !go1.9 package newrelic import ( "github.com/newrelic/go-agent/v3/internal" ) func newTraceObserver(runID internal.AgentRunID, requestHeadersMap map[string]string, cfg observerConfig) (traceObserver, error) { return nil, errUnsupportedVersion } const ( // versionSupports8T records whether we are using a supported version of Go for // Infinite Tracing versionSupports8T = false grpcVersion = "not-installed" ) func expectObserverEvents(v internal.Validator, events *analyticsEvents, expect []internal.WantEvent, extraAttributes map[string]interface{}) { } go-agent-3.42.0/v3/newrelic/trace_observer_1_8_test.go000066400000000000000000000011341510742411500225020ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !go1.9 // +build !go1.9 // This build tag is necessary because Infinite Tracing is only supported for Go version 1.9 and up package newrelic import "testing" func TestSupported8TVersion(t *testing.T) { _, err := NewApplication( ConfigLicense("1234567890123456789012345678901234567890"), ConfigAppName("name"), func(c *Config) { c.InfiniteTracing.TraceObserver.Host = "localhost" }, ) if nil == err { t.Error("expected unsupported version error for 8T but got none") } } go-agent-3.42.0/v3/newrelic/trace_observer_common.go000066400000000000000000000041271510742411500223510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "errors" "time" "github.com/newrelic/go-agent/v3/internal" ) type traceObserver interface { // restart reconnects to the remote trace observer with the given runID and // request headers. restart(runID internal.AgentRunID, requestHeadersMap map[string]string) // shutdown initiates a shutdown of the trace observer and blocks until either // shutdown is complete or the given timeout is hit. shutdown(time.Duration) error // consumeSpan enqueues the span to be sent to the remote trace observer consumeSpan(*spanEvent) // dumpSupportabilityMetrics returns a map of string to float to be turned into metrics dumpSupportabilityMetrics() map[string]float64 // initialConnCompleted indicates that the initial connection to the remote trace // observer was made, but it does NOT indicate anything about the current state of the // connection initialConnCompleted() bool } type observerConfig struct { // endpoint includes data about connecting to the remote trace observer endpoint observerURL // license is the New Relic License key license string // log will be used for logging log Logger // queueSize is the size of the channel used to send span events to // the remote trace observer queueSize int // appShutdown communicates to the trace observer when the application has // completed shutting down appShutdown chan struct{} // dialer is only used for testing - it allows the trace observer to connect directly // to an in-memory gRPC server dialer internal.DialerFunc // removeBackoff sets the recordSpanBackoff to 0 and is useful for testing removeBackoff bool } type observerURL struct { host string secure bool } const ( localTestingHost = "localhost" ) var ( errUnsupportedVersion = errors.New("non supported Go version - to use Infinite Tracing, " + "you must use at least version 1.9 or higher of Go") errSpanOrDTDisabled = errors.New("in order to enable Infinite Tracing, you must have both " + "Distributed Tracing and Span Events enabled") ) go-agent-3.42.0/v3/newrelic/trace_observer_impl_test.go000066400000000000000000000151541510742411500230630ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "context" "fmt" "io" "net" "reflect" "strings" "sync" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/test/bufconn" "github.com/newrelic/go-agent/v3/internal" v1 "github.com/newrelic/go-agent/v3/internal/com_newrelic_trace_v1" "github.com/newrelic/go-agent/v3/internal/logger" ) // This file contains helper functions for Trace Observer tests func expectSupportabilityMetrics(t *testing.T, to traceObserver, expected map[string]float64) { t.Helper() actual := to.dumpSupportabilityMetrics() if !reflect.DeepEqual(expected, actual) { t.Errorf("Supportability metrics do not match.\nExpected: %#v\nActual: %#v\n", expected, actual) } } func createServerAndObserver(t *testing.T) (testObsServer, traceObserver) { s := newTestObsServer(t, simpleRecordSpan) cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) return s, to } type recordSpanFunc func(*expectServer, v1.IngestService_RecordSpanServer) error type expectServer struct { metadata metadata.MD sync.Mutex spansReceivedChan chan struct{} recordSpanFunc recordSpanFunc v1.UnimplementedIngestServiceServer } func (s *expectServer) RecordSpan(stream v1.IngestService_RecordSpanServer) error { return s.recordSpanFunc(s, stream) } func simpleRecordSpan(s *expectServer, stream v1.IngestService_RecordSpanServer) error { md, ok := metadata.FromIncomingContext(stream.Context()) if ok { s.Lock() s.metadata = md s.Unlock() } for { _, err := stream.Recv() if err == io.EOF { return nil } else if nil != err { return err } s.spansReceivedChan <- struct{}{} } } func (s *expectServer) ExpectMetadata(t *testing.T, want map[string]string) { t.Helper() s.Lock() actualMetadata := s.metadata s.Unlock() extraMetadata := map[string]string{ ":authority": internal.MatchAnyString, "content-type": internal.MatchAnyString, "user-agent": internal.MatchAnyString, } want = mergeMetadata(want, extraMetadata) if len(want) != len(actualMetadata) { t.Error("length of metadata is incorrect: expected/actual", len(want), len(actualMetadata)) return } for key, expectedVal := range want { found, ok := actualMetadata[key] actualVal := strings.Join(found, ",") if !ok { t.Error("expected metadata not found: ", key) continue } if expectedVal == internal.MatchAnyString { continue } if actualVal != expectedVal { t.Error("metadata value difference - expected/actual", fmt.Sprintf("key=%s", key), expectedVal, actualVal) } } for key, val := range actualMetadata { _, ok := want[key] if !ok { t.Error("unexpected metadata present", key, val) continue } } } // Add the `extraMetadata` to each of the maps in the `want` parameter. // The data in `want` takes precedence over the `extraMetadata`. If `want` is // nil, returns nil. func mergeMetadata(want map[string]string, extraMetadata map[string]string) map[string]string { if nil == want { return nil } newMap := make(map[string]string) for k, v := range extraMetadata { newMap[k] = v } for k, v := range want { newMap[k] = v } return newMap } // testObsServer contains an in-memory grpc.Server and associated information // needed to connect to it and verify the data it receives type testObsServer struct { *expectServer server *grpc.Server conn *grpc.ClientConn dialer internal.DialerFunc } func (ts *testObsServer) Close() { ts.conn.Close() ts.server.Stop() } // newTestObsServer creates a new testObsServer for use in testing. Be sure // to Close() the server when done with it. func newTestObsServer(t *testing.T, fn recordSpanFunc) testObsServer { grpcServer := grpc.NewServer() s := &expectServer{ // Hard coding the buffer to 10 for now, but it could be variable if needed later. spansReceivedChan: make(chan struct{}, 10), recordSpanFunc: fn, } v1.RegisterIngestServiceServer(grpcServer, s) lis := bufconn.Listen(1024 * 1024) go grpcServer.Serve(lis) bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } conn, err := grpc.Dial("bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure(), grpc.WithBlock(), // create the connection synchronously ) if err != nil { t.Fatal("failure to create ClientConn", err) } return testObsServer{ expectServer: s, server: grpcServer, conn: conn, dialer: bufDialer, } } // DidSpansArrive blocks until at least the expected number of spans arrives, or the timeout is reached. // It returns whether or not the expected number of spans did, in fact, arrive. func (s *expectServer) DidSpansArrive(t *testing.T, expected int, timeout time.Duration) bool { t.Helper() var rcvd int ticker := time.NewTicker(timeout) defer ticker.Stop() for { select { case <-s.spansReceivedChan: rcvd++ if rcvd >= expected { return true } case <-ticker.C: t.Logf("INFO: Waited for %d spans but received %d\n", expected, rcvd) return false } } } func (s *expectServer) DidSpansArriveNoTimeout(t *testing.T, expected int) bool { t.Helper() var rcvd int for { select { case <-s.spansReceivedChan: rcvd++ if rcvd >= expected { return true } } } } // testAppBlockOnTrObs is to be used when creating a test application that needs to block // until the trace observer (which should be configured in the cfgfn) has connected. func testAppBlockOnTrObs(replyfn func(*internal.ConnectReply), cfgfn func(*Config), t testing.TB) *expectApp { app := testApp(replyfn, cfgfn, t) app.app.connectTraceObserver(app.app.placeholderRun.Reply) waitForTrObs(t, app.app.trObserver) return &app } func waitForTrObs(t testing.TB, to traceObserver) { deadline := time.Now().Add(3 * time.Second) pollPeriod := 10 * time.Millisecond for { if to.initialConnCompleted() { return } if time.Now().After(deadline) { t.Fatal("Error connecting to trace observer") } time.Sleep(pollPeriod) } } func DTReplyFieldsWithTrObsDialer(d internal.DialerFunc, runToken string) func(*internal.ConnectReply) { return func(reply *internal.ConnectReply) { distributedTracingReplyFields(reply) reply.RunID = internal.AgentRunID(runToken) reply.TraceObsDialer = d } } func toCfgWithTrObserver(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false cfg.DistributedTracer.Enabled = true cfg.InfiniteTracing.TraceObserver.Host = "localhost" } go-agent-3.42.0/v3/newrelic/trace_observer_test.go000066400000000000000000000701731510742411500220440ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "context" "errors" "net" "reflect" "testing" "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/newrelic/go-agent/v3/internal" v1 "github.com/newrelic/go-agent/v3/internal/com_newrelic_trace_v1" "github.com/newrelic/go-agent/v3/internal/logger" ) func TestValidateTraceObserverURL(t *testing.T) { testcases := []struct { inputHost string inputPort int expectErr bool expectURL *observerURL }{ { inputHost: "", expectErr: false, expectURL: nil, }, { inputHost: "testing.com", expectErr: false, expectURL: &observerURL{ host: "testing.com:443", secure: true, }, }, { inputHost: "1.2.3.4", expectErr: false, expectURL: &observerURL{ host: "1.2.3.4:443", secure: true, }, }, { inputHost: "testing.com", inputPort: 123, expectErr: false, expectURL: &observerURL{ host: "testing.com:123", secure: true, }, }, { inputHost: "localhost", inputPort: 8080, expectErr: false, expectURL: &observerURL{ host: "localhost:8080", secure: false, }, }, } for _, tc := range testcases { t.Run(tc.inputHost, func(t *testing.T) { c := defaultConfig() c.DistributedTracer.Enabled = true c.SpanEvents.Enabled = true c.InfiniteTracing.TraceObserver.Host = tc.inputHost if tc.inputPort != 0 { c.InfiniteTracing.TraceObserver.Port = tc.inputPort } url, err := c.validateTraceObserverConfig() if tc.expectErr && err == nil { t.Error("expected error, received nil") } else if !tc.expectErr && err != nil { t.Errorf("expected no error, but got one: %s", err) } if !reflect.DeepEqual(url, tc.expectURL) { t.Errorf("url is not as expected: actual=%#v expect=%#v", url, tc.expectURL) } }) } } func Test8TConfig(t *testing.T) { testcases := []struct { host string spansEnabled bool DTEnabled bool validConfig bool }{ { host: "localhost", spansEnabled: true, DTEnabled: true, validConfig: true, }, { host: "localhost", spansEnabled: false, DTEnabled: true, validConfig: false, }, { host: "localhost", spansEnabled: true, DTEnabled: false, validConfig: false, }, { host: "localhost", spansEnabled: false, DTEnabled: false, validConfig: false, }, { host: "", spansEnabled: false, DTEnabled: false, validConfig: true, }, } for _, test := range testcases { cfg := Config{} cfg.License = testLicenseKey cfg.AppName = "app" cfg.InfiniteTracing.TraceObserver.Host = test.host cfg.SpanEvents.Enabled = test.spansEnabled cfg.DistributedTracer.Enabled = test.DTEnabled _, err := newInternalConfig(cfg, func(s string) string { return "" }, []string{}) if (err == nil) != test.validConfig { t.Errorf("Infite Tracing config validation failed: %v", test) } } } func TestTraceObserverErrToCodeString(t *testing.T) { // if the grpc code names change upstream, this test will alert us to that testcases := []struct { code codes.Code expect string }{ {code: 0, expect: "OK"}, {code: 1, expect: "CANCELLED"}, {code: 2, expect: "UNKNOWN"}, {code: 3, expect: "INVALID_ARGUMENT"}, {code: 4, expect: "DEADLINE_EXCEEDED"}, {code: 5, expect: "NOT_FOUND"}, {code: 6, expect: "ALREADY_EXISTS"}, {code: 7, expect: "PERMISSION_DENIED"}, {code: 8, expect: "RESOURCE_EXHAUSTED"}, {code: 9, expect: "FAILED_PRECONDITION"}, {code: 10, expect: "ABORTED"}, {code: 11, expect: "OUT_OF_RANGE"}, {code: 12, expect: "UNIMPLEMENTED"}, {code: 13, expect: "INTERNAL"}, {code: 14, expect: "UNAVAILABLE"}, {code: 15, expect: "DATA_LOSS"}, {code: 16, expect: "UNAUTHENTICATED"}, // we should always test one more than the number of codes supported by // grpc so we can detect when a new code is added {code: 17, expect: "CODE(17)"}, } for _, test := range testcases { t.Run(test.expect, func(t *testing.T) { err := status.Error(test.code, "oops") actual := errToCodeString(err) if actual != test.expect { t.Errorf("incorrect error string returned: actual=%s expected=%s", actual, test.expect) } }) } } type mockClient struct { sendResponse error v1.IngestService_RecordSpanClient } func (c mockClient) Send(*v1.Span) error { return c.sendResponse } func TestSendSpanMetrics(t *testing.T) { appShutdown := make(chan struct{}) to := &gRPCtraceObserver{ supportability: newObserverSupport(), observerConfig: observerConfig{ log: logger.ShimLogger{}, appShutdown: appShutdown, }, } go to.handleSupportability() defer close(appShutdown) clientWithError := mockClient{ sendResponse: errPermissionDenied, } clientWithoutError := mockClient{ sendResponse: nil, } // The Seen count will be 0 for each example in this test because Seen is // incremented during consumeSpan which is never called here. expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 0, }) if err := to.sendSpan(clientWithError, &spanEvent{}); err == nil { t.Error("spendSpan should have returned an error when Send returns an error") } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Response/Error": 1, "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 1, "Supportability/InfiniteTracing/Span/gRPC/PERMISSION_DENIED": 1, }) if err := to.sendSpan(clientWithoutError, &spanEvent{}); err != nil { t.Error("spendSpan should not have returned an error when Send returns a nil error") } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 1, }) } const runToken = "aRunToken" func TestTraceObserverRestart(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, } to, err := newTraceObserver(runToken, map[string]string{"INITIAL": "VALUE1"}, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) defer s.Close() // Make sure the server has received the new data to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 1, 150*time.Millisecond) { t.Error("Did not receive expected spans before timeout -- before restart") } s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, "initial": "VALUE1", }) newToken := "aNewRunToken" to.restart(internal.AgentRunID(newToken), map[string]string{"RESTART": "VALUE2"}) // Make sure the server has received the new data to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 1, 150*time.Millisecond) { t.Error("Did not receive expected spans before timeout -- after restart") } s.ExpectMetadata(t, map[string]string{ "agent_run_token": newToken, "license_key": testLicenseKey, "restart": "VALUE2", }) } func TestTraceObserverShutdown(t *testing.T) { s, to := createServerAndObserver(t) s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, }) if err := to.shutdown(time.Second); err != nil { t.Fatal(err) } to.consumeSpan(&spanEvent{}) if s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Got a span we did not expect to get") } s.Close() shutdownApp(to) to.consumeSpan(&spanEvent{}) if s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Got a span we did not expect to get") } } // shutdownApp simulates the whole app shutting down func shutdownApp(to traceObserver) { close(to.(*gRPCtraceObserver).appShutdown) } func TestTraceObserverConsumeSpan(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, }) to.consumeSpan(&spanEvent{}) to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 2, 50*time.Millisecond) { t.Error("Did not receive expected spans before timeout") } } func TestTraceObserverDumpSupportabilityMetrics(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 0, }) to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Did not receive expected spans before timeout") } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 1, "Supportability/InfiniteTracing/Span/Sent": 1, }) // Ensure counts are reset expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 0, }) } func TestTraceObserverConnected(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } if to.initialConnCompleted() { t.Error("Didn't expect the trace observer to be connected, but it is") } readyChan <- struct{}{} waitForTrObs(t, to) if !to.initialConnCompleted() { t.Error("Expected the trace observer to be connected, but it isn't") } } func TestTrObsMultipleShutdowns(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() waitForTrObs(t, to) if err := to.shutdown(time.Second); err != nil { t.Fatal(err) } // Make sure we don't panic if err := to.shutdown(time.Second); err != nil { t.Error("error shutting down the trace observer:", err) } shutdownApp(to) // Make sure we don't panic if err := to.shutdown(time.Second); err != nil { t.Error("error shutting downt the trace observer after shutting down app:", err) } } func TestTrObsShutdownAndRestart(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() waitForTrObs(t, to) if err := to.shutdown(time.Second); err != nil { t.Fatal(err) } // Make sure we don't panic and don't send updated metadata to.restart("A New Run Token", map[string]string{"hello": "world"}) s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, }) shutdownApp(to) // Make sure we don't panic and don't send updated metadata to.restart("A New Run Token", map[string]string{"hello": "world"}) s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, }) } func TestTrObsShutdownAndInitialConnSuccessful(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() waitForTrObs(t, to) if err := to.shutdown(time.Second); err != nil { t.Fatal(err) } if !to.initialConnCompleted() { t.Error("Expected the initialConnCompleted call to return true after shutdown, " + "but returned false") } shutdownApp(to) if !to.initialConnCompleted() { t.Error("Expected the initialConnCompleted call to return true after app shutdown, " + "but returned false") } } func TestTrObsShutdownAndDumpSupportabilityMetrics(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() if err := to.shutdown(time.Second); err != nil { t.Fatal(err) } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 0, // the error metrics are from the EOF on the client.Recv "Supportability/InfiniteTracing/Span/Response/Error": 1, "Supportability/InfiniteTracing/Span/gRPC/UNKNOWN": 1, }) shutdownApp(to) expectSupportabilityMetrics(t, to, nil) } func TestTrObsSlowConnectAndRestart(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, map[string]string{"INITIAL": "ONE"}, cfg) if nil != err { t.Fatal(err) } newToken := "A New Run Token" to.restart(internal.AgentRunID(newToken), map[string]string{"RESTART": "TWO"}) if s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Got a span we did not expect to get") } s.ExpectMetadata(t, nil) close(readyChan) if s.DidSpansArrive(t, 1, 500*time.Millisecond) { t.Error("Got a span we did not expect to get") } s.ExpectMetadata(t, map[string]string{ "agent_run_token": newToken, "license_key": testLicenseKey, "restart": "TWO", }) } func TestTrObsSlowConnectAndConsumeSpan(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } to.consumeSpan(&spanEvent{}) if s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Got a span we did not expect to get") } close(readyChan) if !s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Did not receive expected spans before timeout") } } func TestTrObsSlowConnectAndDumpSupportabilityMetrics(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 0, }) to.consumeSpan(&spanEvent{}) expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 1, "Supportability/InfiniteTracing/Span/Sent": 0, }) close(readyChan) if !s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("Did not receive expected spans before timeout") } expectSupportabilityMetrics(t, to, map[string]float64{ "Supportability/InfiniteTracing/Span/Seen": 0, "Supportability/InfiniteTracing/Span/Sent": 1, }) } func toIsShutdown(to traceObserver) bool { // This sleep is so long because it is waiting on the deferred 500 // millisecond sleep for closing the grpc conn. time.Sleep(550 * time.Millisecond) return to.(*gRPCtraceObserver).isShutdownComplete() } func TestTrObsSlowConnectAndShutdown(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } to.consumeSpan(&spanEvent{}) if err := to.shutdown(time.Nanosecond); err == nil { t.Error("trace observer was able to shutdown when it shouldn't have") } close(readyChan) if !toIsShutdown(to) { t.Error("trace observer should be shutdown but it is not") } if !s.DidSpansArrive(t, 1, 50*time.Millisecond) { t.Error("span was not received") } } var ( errUnimplemented = status.Error(codes.Unimplemented, "unimplemented") errPermissionDenied = status.Error(codes.PermissionDenied, "I'm so sorry") errOK = status.Error(codes.OK, "okay okay okay") // grpc turns this into nil ) func TestTrObsRecordSpanReturnsError(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() errDialer := func(context.Context, string) (net.Conn, error) { // It doesn't matter what error is returned here, grpc will translate // this into a code 14 error. This error is returned from RecordSpan // and since it is not an Unimplemented error, we will not shut down. return nil, errors.New("ooooops") } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: errDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } if toIsShutdown(to) { t.Error("trace observer should not be shutdown but it is") } } func TestTrObsRecvReturnsUnimplementedError(t *testing.T) { s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { return errUnimplemented }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) if !toIsShutdown(to) { t.Error("trace observer should be shutdown but it is not") } } func TestTrObsRecvReturnsOtherError(t *testing.T) { s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { return errPermissionDenied }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) if toIsShutdown(to) { t.Error("trace observer should not be shutdown but it is") } } func TestTrObsUnimplementedNoMoreSpansSent(t *testing.T) { s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { stream.Recv() s.spansReceivedChan <- struct{}{} return errUnimplemented }) cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: true, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) // First span should cause a shutdown to initiate; // the others should get queued but may or may not be not sent to.consumeSpan(&spanEvent{}) to.consumeSpan(&spanEvent{}) to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 1, time.Second) { t.Error("Did not receive expected span before timeout") } if !toIsShutdown(to) { t.Error("trace observer should be shutdown but it is not") } // Closing the server ensures that if a span was sent that it will be // received and read by the server s.Close() // Additional spans should not be delivered if s.DidSpansArrive(t, 1, 100*time.Millisecond) { t.Error("Received 1 spans after shutdown when we should not receive any") } } func TestTrObsPermissionDeniedMoreSpansSent(t *testing.T) { s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { stream.Recv() s.spansReceivedChan <- struct{}{} return errPermissionDenied }) cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: true, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) to.consumeSpan(&spanEvent{}) to.consumeSpan(&spanEvent{}) if !s.DidSpansArrive(t, 1, time.Second) { t.Error("Did not receive expected span before timeout") } if toIsShutdown(to) { t.Error("trace observer should not be shutdown but it is") } // Closing the server ensures that if a span was sent that it will be // received and read by the server s.Close() // Additional spans should be delivered if !s.DidSpansArrive(t, 1, time.Second) { t.Error("did not receive 1 expected spans") } } func TestTrObsDrainsMessagesOnShutdown(t *testing.T) { s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { return errUnimplemented }) defer s.Close() readyChan := make(chan struct{}) slowDialer := func(ctx context.Context, str string) (net.Conn, error) { <-readyChan return s.dialer(ctx, str) } cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 20, appShutdown: make(chan struct{}), dialer: slowDialer, } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } numMsgs := func() int { return len(to.(*gRPCtraceObserver).messages) } for i := 0; i < 20; i++ { // We must consume a significant number of spans here because between // 2-5 of them will be sent before the unimplemented error is received. to.consumeSpan(&spanEvent{}) } if num := numMsgs(); num != 20 { t.Errorf("there should be 20 spans waiting to be sent but there were %d", num) } close(readyChan) if !toIsShutdown(to) { t.Error("trace observer should be shutdown but it is not") } if num := numMsgs(); num != 0 { t.Errorf("there should be 0 spans waiting to be sent but there were %d", num) } } // Very rarely we would see a data race on shutdown; this test is to reproduce it before fixing it // (and ensuring we don't bring it back in the future) func TestTrObsDetectDataRaceOnShutdown(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() to.consumeSpan(&spanEvent{}) to.consumeSpan(&spanEvent{}) to.shutdown(15 * time.Millisecond) to.consumeSpan(&spanEvent{}) } func TestTrObsConsumingAfterShutdown(t *testing.T) { s, to := createServerAndObserver(t) defer s.Close() for i := 0; i < 5; i++ { to.consumeSpan(&spanEvent{}) } to.shutdown(time.Nanosecond) for i := 0; i < 5; i++ { to.consumeSpan(&spanEvent{}) } if !s.DidSpansArrive(t, 5, time.Second) { t.Error("did not receive initial 5 spans sent before shutdown") } if s.DidSpansArrive(t, 1, time.Second) { t.Error("spans sent after shutdown was called") } } func TestTrObsOKSendBackoffNo(t *testing.T) { // In this test, the OK response will be noticed by sendSpan s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { stream.Recv() s.spansReceivedChan <- struct{}{} return errOK }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 200, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: false, // ensure that the backoff remains for non-OK responses } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) // The grpc client will internally cache spans before sending them to // ensure a minimum number of bytes are sent with each batch. Because of // this we'll queue up more than enough spans to force at least two of them // to get sent and received. for i := 0; i < 200; i++ { to.consumeSpan(&spanEvent{}) } // If the default backoff of 15 seconds is used, the second span will not // be received in time. if !s.DidSpansArriveNoTimeout(t, 1) { t.Error("server did not receive a span") } } func TestTrObsOKReceiveBackoffNo(t *testing.T) { // In this test, the OK response will be noticed by Recv var count int s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { count++ if count == 1 { return errOK } for { stream.Recv() s.spansReceivedChan <- struct{}{} } }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 200, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: false, // ensure that the backoff remains for non-OK responses } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) // The grpc client will internally cache spans before sending them to // ensure a minimum number of bytes are sent with each batch. Because of // this we'll queue up more than enough spans to force at least two of them // to get sent and received. for i := 0; i < 200; i++ { to.consumeSpan(&spanEvent{}) } // If the default backoff of 15 seconds is used, the second span will not // be received in time. if !s.DidSpansArriveNoTimeout(t, 1) { t.Error("server did not receive a span") } } func TestTrObsPermissionDeniedSendBackoffYes(t *testing.T) { // In this test, the Permission Denied response will be noticed by sendSpan s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { stream.Recv() s.spansReceivedChan <- struct{}{} return errPermissionDenied }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 200, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: false, // ensure that the backoff remains for non-OK responses } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) // The grpc client will internally cache spans before sending them to // ensure a minimum number of bytes are sent with each batch. Because of // this we'll queue up more than enough spans to force them to get sent. for i := 0; i < 200; i++ { to.consumeSpan(&spanEvent{}) } if !s.DidSpansArrive(t, 1, time.Second) { t.Error("server did not receive initial span") } // Since the default backoff of 15 seconds is used, the second span will not // be received in time. if s.DidSpansArrive(t, 1, time.Second) { t.Error("server received a second span when it should not have") } } func TestTrObsPermissionDeniedReceiveBackoffYes(t *testing.T) { // In this test, the Permission Denied response will be noticed by Recv var count int s := newTestObsServer(t, func(s *expectServer, stream v1.IngestService_RecordSpanServer) error { count++ if count == 1 { return errPermissionDenied } for { stream.Recv() s.spansReceivedChan <- struct{}{} } }) defer s.Close() cfg := observerConfig{ log: logger.ShimLogger{}, license: testLicenseKey, queueSize: 200, appShutdown: make(chan struct{}), dialer: s.dialer, removeBackoff: false, // ensure that the backoff remains for non-OK responses } to, err := newTraceObserver(runToken, nil, cfg) if nil != err { t.Fatal(err) } waitForTrObs(t, to) // The grpc client will internally cache spans before sending them to // ensure a minimum number of bytes are sent with each batch. Because of // this we'll queue up more than enough spans to force them to get sent. for i := 0; i < 200; i++ { to.consumeSpan(&spanEvent{}) } // Since the default backoff of 15 seconds is used, even the first span // will not be received in time. if s.DidSpansArrive(t, 1, time.Second) { t.Error("server received a span when it should not have") } } /******************** * Integration test * ********************/ func TestTraceObserverRoundTrip(t *testing.T) { s := newTestObsServer(t, simpleRecordSpan) defer s.Close() runToken := "aRunToken" app := testAppBlockOnTrObs(DTReplyFieldsWithTrObsDialer(s.dialer, runToken), toCfgWithTrObserver, t) txn := app.StartTransaction("txn1") txn.StartSegment("seg1").End() txn.End() app.Shutdown(10 * time.Second) app.expectNoLoggedErrors(t) // Ensure no spans were sent the normal way app.ExpectSpanEvents(t, nil) if !s.DidSpansArrive(t, 2, time.Second) { t.Error("Did not receive expected spans before timeout") } s.ExpectMetadata(t, map[string]string{ "agent_run_token": runToken, "license_key": testLicenseKey, }) } go-agent-3.42.0/v3/newrelic/tracing.go000066400000000000000000000620251510742411500174240ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "errors" "fmt" "net/http" "net/url" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" "github.com/newrelic/go-agent/v3/internal/jsonx" "github.com/newrelic/go-agent/v3/internal/logger" ) // txnEvent represents a transaction. // https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Events-PORTED.md // https://newrelic.atlassian.net/wiki/display/eng/Agent+Support+for+Synthetics%3A+Forced+Transaction+Traces+and+Analytic+Events type txnEvent struct { HasError bool FinalName string Attrs *attributes CrossProcess txnCrossProcess BetterCAT betterCAT Start time.Time Duration time.Duration TotalTime time.Duration Queuing time.Duration Zone apdexZone externalCallCount uint64 externalDuration time.Duration datastoreCallCount uint64 datastoreDuration time.Duration errGroupCallback ErrorGroupCallback TxnID string } // betterCAT stores the transaction's priority and all fields related // to a DistributedTracer's Cross-Application Trace. type betterCAT struct { Enabled bool Sampled bool Priority priority TxnID string TraceID string TransportType string Inbound *payload } // SetTraceAndTxnIDs takes a single 32 character ID and uses it to // set both the trace (32 char) and transaction (16 char) ID. func (bc *betterCAT) SetTraceAndTxnIDs(traceID string) { txnLength := 16 bc.TraceID = traceID if len(traceID) <= txnLength { bc.TxnID = traceID } else { bc.TxnID = traceID[:txnLength] } } func (e *txnEvent) SetTransactionID(transactionID string) { txnLength := 16 if len(transactionID) <= txnLength { e.TxnID = transactionID } else { e.TxnID = transactionID[:txnLength] } } // txnData contains the recorded data of a transaction. type txnData struct { IsWeb bool SlowQueriesEnabled bool noticeErrors bool // If errors are not expected or ignored, then true expectedErrors bool ignoreApdex bool stamp segmentStamp threadIDCounter uint64 Name string // Work in progress name. rootSpanID string txnEvent TxnTrace txnTrace Stop time.Time ApdexThreshold time.Duration SlowQueryThreshold time.Duration SlowQueries *slowQueries // These better CAT supportability fields are left outside of // TxnEvent.BetterCAT to minimize the size of transaction event memory. DistributedTracingSupport distributedTracingSupport TraceIDGenerator *internal.TraceIDGenerator ShouldCollectSpanEvents func() bool ShouldCreateSpanGUID func() bool rootSpanErrData *errorData Errors txnErrors // Lazily initialized. SpanEvents []*spanEvent logs logEventHeap customSegments map[string]*metricData datastoreSegments map[datastoreMetricKey]*metricData externalSegments map[externalMetricKey]*metricData messageSegments map[internal.MessageMetricKey]*metricData } func (t *txnData) saveTraceSegment(end segmentEnd, name string, attrs spanAttributeMap, externalGUID string) { attrs = t.Attrs.filterSpanAttributes(attrs, destSegment) t.TxnTrace.witnessNode(end, name, attrs, externalGUID) } // tracingThread contains a segment stack that is used to track segment parenting time // within a single goroutine. type tracingThread struct { threadID uint64 stack []segmentFrame // start and end are used to track the TotalTime this tracingThread was active. start time.Time end time.Time } // RecordActivity indicates that activity happened at this time on this // goroutine which helps track total time. func (thread *tracingThread) RecordActivity(now time.Time) { if thread.start.IsZero() || now.Before(thread.start) { thread.start = now } if now.After(thread.end) { thread.end = now } } // TotalTime returns the amount to time that this thread contributes to the // total time. func (thread *tracingThread) TotalTime() time.Duration { if thread.start.Before(thread.end) { return thread.end.Sub(thread.start) } return 0 } // newTracingThread returns a new tracingThread to track segments in a new goroutine. func newTracingThread(txndata *txnData) *tracingThread { // Each thread needs a unique ID. txndata.threadIDCounter++ return &tracingThread{ threadID: txndata.threadIDCounter, } } type segmentStamp uint64 type segmentTime struct { Stamp segmentStamp Time time.Time } // segmentStartTime is embedded into the top level segments (rather than // segmentTime) to minimize the structure sizes to minimize allocations. type segmentStartTime struct { Stamp segmentStamp Depth int } type stringJSONWriter string func (s stringJSONWriter) WriteJSON(buf *bytes.Buffer) { jsonx.AppendString(buf, string(s)) } type intJSONWriter int func (i intJSONWriter) WriteJSON(buf *bytes.Buffer) { jsonx.AppendInt(buf, int64(i)) } type floatJSONWriter float64 func (f floatJSONWriter) WriteJSON(buf *bytes.Buffer) { // Note: we validate the float earlier on in the stack, so we don't need // to worry about checking the error here. _ = jsonx.AppendFloat(buf, float64(f)) } type boolJSONWriter bool func (b boolJSONWriter) WriteJSON(buf *bytes.Buffer) { if b { buf.WriteString("true") } else { buf.WriteString("false") } } // spanAttributeMap is used for span attributes and segment attributes. The // value is a jsonWriter to allow for segment query parameters. type spanAttributeMap map[string]jsonWriter // addString writes the span attribute and checks for handling the special // case for db.statement func (m *spanAttributeMap) addString(key string, val string) { if val != "" { if key == SpanAttributeDBStatement { m.add(key, stringJSONWriter(truncateSpanAttribute(val, attributeSpanDBStatementLimit))) } else { m.add(key, stringJSONWriter(stringLengthByteLimit(val, attributeValueLengthLimit))) } } } func (m *spanAttributeMap) addInt(key string, val int) { m.add(key, intJSONWriter(val)) } func (m *spanAttributeMap) addBool(key string, val bool) { m.add(key, boolJSONWriter(val)) } func (m *spanAttributeMap) addFloat(key string, val float64) { m.add(key, floatJSONWriter(val)) } func (m *spanAttributeMap) add(key string, val jsonWriter) { if *m == nil { *m = make(spanAttributeMap) } (*m)[key] = val } func (m spanAttributeMap) copy() spanAttributeMap { if len(m) == 0 { return nil } cpy := make(spanAttributeMap, len(m)) for k, v := range m { cpy[k] = v } return cpy } func (m *spanAttributeMap) addUserAttrs(attrs map[string]userAttribute) { for key, val := range attrs { if val.dests&destSpan > 0 { addAttr(m, key, val.value) } } } func (m *spanAttributeMap) addAgentAttrs(attrs agentAttributes) { for key, val := range attrs { if val.stringVal != "" { m.addString(key, val.stringVal) } else { addAttr(m, key, val.otherVal) } } } func addAttr(m *spanAttributeMap, key string, val any) { switch v := val.(type) { case string: m.addString(key, v) case bool: m.addBool(key, v) case uint8: m.addInt(key, int(v)) case uint16: m.addInt(key, int(v)) case uint32: m.addInt(key, int(v)) case uint64: m.addInt(key, int(v)) case uint: m.addInt(key, int(v)) case uintptr: m.addInt(key, int(v)) case int8: m.addInt(key, int(v)) case int16: m.addInt(key, int(v)) case int32: m.addInt(key, int(v)) case int64: m.addInt(key, int(v)) case int: m.addInt(key, v) case float32: m.addFloat(key, float64(v)) case float64: m.addFloat(key, v) default: m.addString(key, fmt.Sprintf("%T", v)) } } type segmentFrame struct { segmentTime children time.Duration spanID string agentAttributes spanAttributeMap userAttributes spanAttributeMap } type segmentEnd struct { start segmentTime stop segmentTime duration time.Duration exclusive time.Duration SpanID string ParentID string threadID uint64 agentAttributes spanAttributeMap userAttributes spanAttributeMap } func (end segmentEnd) spanEvent() *spanEvent { if end.SpanID == "" { return nil } return &spanEvent{ GUID: end.SpanID, ParentID: end.ParentID, Timestamp: end.start.Time, Duration: end.duration, AgentAttributes: end.agentAttributes, UserAttributes: end.userAttributes, IsEntrypoint: false, } } const ( datastoreProductUnknown = "Unknown" datastoreOperationUnknown = "other" ) // NoticeErrors indicates whether the errors collected count towards error/ metrics func (t *txnData) NoticeErrors() bool { return t.noticeErrors } // HasErrors indicates whether the transaction had errors. func (t *txnData) HasErrors() bool { return len(t.Errors) > 0 } // HasExpectedErrors is a special case where the txn has errors but we dont increment error metrics func (t *txnData) HasExpectedErrors() bool { return t.expectedErrors } func (t *txnData) time(now time.Time) segmentTime { // Update the stamp before using it so that a 0 stamp can be special. t.stamp++ return segmentTime{ Time: now, Stamp: t.stamp, } } // AddAgentSpanAttribute allows attributes to be added to spans. func (thread *tracingThread) AddAgentSpanAttribute(key string, val string) { if len(thread.stack) > 0 { thread.stack[len(thread.stack)-1].agentAttributes.addString(key, val) } } // AddUserSpanAttribute allows custom attributes to be added to spans. func (thread *tracingThread) AddUserSpanAttribute(key string, val any) { if len(thread.stack) > 0 { userAttributes := &thread.stack[len(thread.stack)-1].userAttributes userAttributes.addUserAttrs(map[string]userAttribute{ key: { value: val, dests: destAll, }, }) } } // RemoveErrorSpanAttribute allows attributes to be removed from spans. func (thread *tracingThread) RemoveErrorSpanAttribute(key string) { stackLen := len(thread.stack) if stackLen <= 0 { return } delete(thread.stack[stackLen-1].agentAttributes, key) } // startSegment begins a segment. func startSegment(t *txnData, thread *tracingThread, now time.Time) segmentStartTime { tm := t.time(now) thread.stack = append(thread.stack, segmentFrame{ segmentTime: tm, children: 0, }) return segmentStartTime{ Stamp: tm.Stamp, Depth: len(thread.stack) - 1, } } // GetRootSpanID returns the root span ID. func (t *txnData) GetRootSpanID() string { if t.rootSpanID == "" { t.rootSpanID = t.TraceIDGenerator.GenerateSpanID() } return t.rootSpanID } // CurrentSpanIdentifier returns the identifier of the span at the top of the // segment stack. func (t *txnData) CurrentSpanIdentifier(thread *tracingThread) string { if len(thread.stack) == 0 { return t.GetRootSpanID() } if thread.stack[len(thread.stack)-1].spanID == "" { thread.stack[len(thread.stack)-1].spanID = t.TraceIDGenerator.GenerateSpanID() } return thread.stack[len(thread.stack)-1].spanID } func (t *txnData) saveSpanEvent(e *spanEvent) { e.AgentAttributes = t.Attrs.filterSpanAttributes(e.AgentAttributes, destSpan) if len(t.SpanEvents) < internal.MaxSpanEvents { t.SpanEvents = append(t.SpanEvents, e) } } var ( errMalformedSegment = errors.New("segment identifier malformed: perhaps unsafe code has modified it?") // errSegmentOrder indicates that segments have been ended in the // incorrect order. errSegmentOrder = errors.New(`improper segment use: segments must be ended in "last started first ended" order: ` + `use https://godoc.org/github.com/newrelic/go-agent/v3/newrelic#Transaction.NewGoroutine to use the transaction in multiple goroutines`) ) func endSegment(t *txnData, thread *tracingThread, start segmentStartTime, now time.Time) (segmentEnd, error) { if start.Stamp == 0 { return segmentEnd{}, errMalformedSegment } if start.Depth >= len(thread.stack) { return segmentEnd{}, errSegmentOrder } if start.Depth < 0 { return segmentEnd{}, errMalformedSegment } frame := thread.stack[start.Depth] if start.Stamp != frame.Stamp { return segmentEnd{}, errSegmentOrder } var children time.Duration for i := start.Depth; i < len(thread.stack); i++ { children += thread.stack[i].children } s := segmentEnd{ stop: t.time(now), start: frame.segmentTime, agentAttributes: frame.agentAttributes, userAttributes: frame.userAttributes, } if s.stop.Time.After(s.start.Time) { s.duration = s.stop.Time.Sub(s.start.Time) } if s.duration > children { s.exclusive = s.duration - children } // Note that we expect (depth == (len(t.stack) - 1)). However, if // (depth < (len(t.stack) - 1)), that's ok: could be a panic popped // some stack frames (and the consumer was not using defer). if start.Depth > 0 { thread.stack[start.Depth-1].children += s.duration } thread.stack = thread.stack[0:start.Depth] if fn := t.ShouldCreateSpanGUID; fn != nil && fn() { s.SpanID = frame.spanID if s.SpanID == "" { s.SpanID = t.TraceIDGenerator.GenerateSpanID() } } if fn := t.ShouldCollectSpanEvents; fn != nil && fn() { // Note that the current span identifier is the parent's // identifier because we've already popped the segment that's // ending off of the stack. s.ParentID = t.CurrentSpanIdentifier(thread) } s.threadID = thread.threadID thread.RecordActivity(s.start.Time) thread.RecordActivity(s.stop.Time) return s, nil } // endBasicSegment ends a basic segment. func endBasicSegment(t *txnData, thread *tracingThread, start segmentStartTime, now time.Time, name string) error { end, err := endSegment(t, thread, start, now) if err != nil { return err } if nil == t.customSegments { t.customSegments = make(map[string]*metricData) } m := metricDataFromDuration(end.duration, end.exclusive) if data, ok := t.customSegments[name]; ok { data.aggregate(m) } else { // Use `new` in place of &m so that m is not // automatically moved to the heap. cpy := new(metricData) *cpy = m t.customSegments[name] = cpy } if t.TxnTrace.considerNode(end) { attributes := end.agentAttributes.copy() t.saveTraceSegment(end, customSegmentMetric(name), attributes, "") } if evt := end.spanEvent(); evt != nil { evt.Name = customSegmentMetric(name) evt.Category = spanCategoryGeneric t.saveSpanEvent(evt) } return nil } // endExternalParams contains the parameters for endExternalSegment. type endExternalParams struct { TxnData *txnData Thread *tracingThread Start segmentStartTime Now time.Time Logger logger.Logger Response *http.Response URL *url.URL Host string Library string Method string StatusCode *int } // endExternalSegment ends an external segment. func endExternalSegment(p endExternalParams) error { t := p.TxnData end, err := endSegment(t, p.Thread, p.Start, p.Now) if err != nil { return err } // Use the Host field if present, otherwise use host in the URL. if p.Host == "" && p.URL != nil { p.Host = p.URL.Host } if p.Host == "" { p.Host = "unknown" } if p.Library == "" { p.Library = "http" } var appData *cat.AppDataHeader if p.Response != nil { hdr := httpHeaderToAppData(p.Response.Header) appData, err = t.CrossProcess.ParseAppData(hdr) if err != nil { if p.Logger.DebugEnabled() { p.Logger.Debug("failure to parse cross application response header", map[string]any{ "err": err.Error(), "header": hdr, }) } } } var crossProcessID string var transactionName string var transactionGUID string if appData != nil { crossProcessID = appData.CrossProcessID transactionName = appData.TransactionName transactionGUID = appData.TransactionGUID } key := externalMetricKey{ Host: p.Host, Library: p.Library, Method: p.Method, ExternalCrossProcessID: crossProcessID, ExternalTransactionName: transactionName, } if t.externalSegments == nil { t.externalSegments = make(map[externalMetricKey]*metricData) } t.externalCallCount++ t.externalDuration += end.duration m := metricDataFromDuration(end.duration, end.exclusive) if data, ok := t.externalSegments[key]; ok { data.aggregate(m) } else { // Use `new` in place of &m so that m is not // automatically moved to the heap. cpy := new(metricData) *cpy = m t.externalSegments[key] = cpy } if t.TxnTrace.considerNode(end) { attributes := end.agentAttributes.copy() if p.Library == "http" { attributes.addString(SpanAttributeHTTPURL, safeURL(p.URL)) } t.saveTraceSegment(end, key.scopedMetric(), attributes, transactionGUID) } if evt := end.spanEvent(); evt != nil { evt.Name = key.scopedMetric() evt.Category = spanCategoryHTTP evt.Kind = "client" evt.Component = p.Library if p.Library == "http" { evt.AgentAttributes.addString(SpanAttributeHTTPURL, safeURL(p.URL)) evt.AgentAttributes.addString(SpanAttributeHTTPMethod, p.Method) } if p.StatusCode != nil { evt.AgentAttributes.addInt(SpanAttributeHTTPStatusCode, *p.StatusCode) } else if p.Response != nil { evt.AgentAttributes.addInt(SpanAttributeHTTPStatusCode, p.Response.StatusCode) } t.saveSpanEvent(evt) } return nil } // endMessageParams contains the parameters for endMessageSegment. type endMessageParams struct { TxnData *txnData Thread *tracingThread Start segmentStartTime Now time.Time Logger logger.Logger DestinationName string Library string DestinationType string DestinationTemp bool } // endMessageSegment ends an external segment. func endMessageSegment(p endMessageParams) error { t := p.TxnData end, err := endSegment(t, p.Thread, p.Start, p.Now) if err != nil { return err } key := internal.MessageMetricKey{ Library: p.Library, DestinationType: p.DestinationType, DestinationName: p.DestinationName, DestinationTemp: p.DestinationTemp, } if t.messageSegments == nil { t.messageSegments = make(map[internal.MessageMetricKey]*metricData) } m := metricDataFromDuration(end.duration, end.exclusive) if data, ok := t.messageSegments[key]; ok { data.aggregate(m) } else { // Use `new` in place of &m so that m is not // automatically moved to the heap. cpy := new(metricData) *cpy = m t.messageSegments[key] = cpy } if t.TxnTrace.considerNode(end) { attributes := end.agentAttributes.copy() t.saveTraceSegment(end, key.Name(), attributes, "") } if evt := end.spanEvent(); evt != nil { evt.Name = key.Name() evt.Category = spanCategoryGeneric t.saveSpanEvent(evt) } return nil } // endDatastoreParams contains the parameters for endDatastoreSegment. type endDatastoreParams struct { TxnData *txnData Thread *tracingThread Start segmentStartTime Now time.Time Product string Collection string Operation string ParameterizedQuery string QueryParameters map[string]any Host string PortPathOrID string Database string ThisHost string } const ( unknownDatastoreHost = "unknown" unknownDatastorePortPathOrID = "unknown" ) var ( hostsToReplace = map[string]struct{}{ "localhost": {}, "127.0.0.1": {}, "0.0.0.0": {}, "0:0:0:0:0:0:0:1": {}, "::1": {}, "0:0:0:0:0:0:0:0": {}, "::": {}, } ) func (t txnData) slowQueryWorthy(d time.Duration) bool { return t.SlowQueriesEnabled && (d >= t.SlowQueryThreshold) } func datastoreSpanAddress(host, portPathOrID string) string { if host != "" && portPathOrID != "" { return host + ":" + portPathOrID } if host != "" { return host } return portPathOrID } // endDatastoreSegment ends a datastore segment. func endDatastoreSegment(p endDatastoreParams) error { end, err := endSegment(p.TxnData, p.Thread, p.Start, p.Now) if err != nil { return err } if p.Operation == "" { p.Operation = datastoreOperationUnknown } if p.Product == "" { p.Product = datastoreProductUnknown } if p.Host == "" && p.PortPathOrID != "" { p.Host = unknownDatastoreHost } if p.PortPathOrID == "" && p.Host != "" { p.PortPathOrID = unknownDatastorePortPathOrID } if _, ok := hostsToReplace[p.Host]; ok { p.Host = p.ThisHost } // We still want to create a slowQuery if the consumer has not provided // a Query string (or it has been removed by LASP) since the stack trace // has value. if p.ParameterizedQuery == "" { collection := p.Collection if collection == "" { collection = "unknown" } p.ParameterizedQuery = fmt.Sprintf(`'%s' on '%s' using '%s'`, p.Operation, collection, p.Product) } key := datastoreMetricKey{ Product: p.Product, Collection: p.Collection, Operation: p.Operation, Host: p.Host, PortPathOrID: p.PortPathOrID, } if p.TxnData.datastoreSegments == nil { p.TxnData.datastoreSegments = make(map[datastoreMetricKey]*metricData) } p.TxnData.datastoreCallCount++ p.TxnData.datastoreDuration += end.duration m := metricDataFromDuration(end.duration, end.exclusive) if data, ok := p.TxnData.datastoreSegments[key]; ok { data.aggregate(m) } else { // Use `new` in place of &m so that m is not // automatically moved to the heap. cpy := new(metricData) *cpy = m p.TxnData.datastoreSegments[key] = cpy } scopedMetric := datastoreScopedMetric(key) // errors in QueryParameters must not stop the recording of the segment queryParams, err := vetQueryParameters(p.QueryParameters) if p.TxnData.TxnTrace.considerNode(end) { attributes := end.agentAttributes.copy() attributes.addString(SpanAttributeDBStatement, p.ParameterizedQuery) attributes.addString(SpanAttributeDBInstance, p.Database) attributes.addString(SpanAttributePeerAddress, datastoreSpanAddress(p.Host, p.PortPathOrID)) attributes.addString(SpanAttributePeerHostname, p.Host) if len(queryParams) > 0 { attributes.add(spanAttributeQueryParameters, queryParams) } p.TxnData.saveTraceSegment(end, scopedMetric, attributes, "") } if p.TxnData.slowQueryWorthy(end.duration) { if nil == p.TxnData.SlowQueries { p.TxnData.SlowQueries = newSlowQueries(maxTxnSlowQueries) } p.TxnData.SlowQueries.observeInstance(slowQueryInstance{ Duration: end.duration, DatastoreMetric: scopedMetric, ParameterizedQuery: p.ParameterizedQuery, QueryParameters: queryParams, Host: p.Host, PortPathOrID: p.PortPathOrID, DatabaseName: p.Database, StackTrace: getStackTrace(), }) } if evt := end.spanEvent(); evt != nil { evt.Name = scopedMetric evt.Category = spanCategoryDatastore evt.Kind = "client" evt.Component = p.Product evt.AgentAttributes.addString(SpanAttributeDBStatement, p.ParameterizedQuery) evt.AgentAttributes.addString(SpanAttributeDBInstance, p.Database) evt.AgentAttributes.addString(SpanAttributePeerAddress, datastoreSpanAddress(p.Host, p.PortPathOrID)) evt.AgentAttributes.addString(SpanAttributePeerHostname, p.Host) evt.AgentAttributes.addString(SpanAttributeDBCollection, p.Collection) p.TxnData.saveSpanEvent(evt) } return err } func truncateSpanAttribute(value string, maxLength int) string { if len(value) > maxLength { // truncate to last three bytes and append "..." return stringLengthByteLimit(value, maxLength-3) + "..." } return value } // MergeBreakdownMetrics creates segment metrics. func mergeBreakdownMetrics(t *txnData, metrics *metricTable) { scope := t.FinalName isWeb := t.IsWeb // Custom Segment Metrics for key, data := range t.customSegments { name := customSegmentMetric(key) // Unscoped metrics.add(name, "", *data, unforced) // Scoped metrics.add(name, scope, *data, unforced) } // External Segment Metrics for key, data := range t.externalSegments { metrics.add(externalRollupMetric.all, "", *data, forced) metrics.add(externalRollupMetric.webOrOther(isWeb), "", *data, forced) hostMetric := externalHostMetric(key) metrics.add(hostMetric, "", *data, unforced) if key.ExternalCrossProcessID != "" && key.ExternalTransactionName != "" { txnMetric := externalTransactionMetric(key) // Unscoped CAT metrics metrics.add(externalAppMetric(key), "", *data, unforced) metrics.add(txnMetric, "", *data, unforced) } // Scoped External Metric metrics.add(key.scopedMetric(), scope, *data, unforced) } // Datastore Segment Metrics for key, data := range t.datastoreSegments { metrics.add(datastoreRollupMetric.all, "", *data, forced) metrics.add(datastoreRollupMetric.webOrOther(isWeb), "", *data, forced) product := datastoreProductMetric(key) metrics.add(product.all, "", *data, forced) metrics.add(product.webOrOther(isWeb), "", *data, forced) if key.Host != "" && key.PortPathOrID != "" { instance := datastoreInstanceMetric(key) metrics.add(instance, "", *data, unforced) } operation := datastoreOperationMetric(key) metrics.add(operation, "", *data, unforced) if key.Collection != "" { statement := datastoreStatementMetric(key) metrics.add(statement, "", *data, unforced) metrics.add(statement, scope, *data, unforced) } else { metrics.add(operation, scope, *data, unforced) } } // Message Segment Metrics for key, data := range t.messageSegments { metric := key.Name() metrics.add(metric, scope, *data, unforced) metrics.add(metric, "", *data, unforced) } } go-agent-3.42.0/v3/newrelic/tracing_test.go000066400000000000000000001251041510742411500204610ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "net/url" "strconv" "strings" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" "github.com/newrelic/go-agent/v3/internal/crossagent" "github.com/newrelic/go-agent/v3/internal/logger" ) func trueFunc() bool { return true } func falseFunc() bool { return false } func TestStartEndSegment(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} token := startSegment(txndata, thread, start) stop := start.Add(1 * time.Second) end, err := endSegment(txndata, thread, token, stop) if nil != err { t.Error(err) } if end.exclusive != end.duration { t.Error(end.exclusive, end.duration) } if end.duration != 1*time.Second { t.Error(end.duration) } if end.start.Time != start { t.Error(end.start, start) } if end.stop.Time != stop { t.Error(end.stop, stop) } if 0 != len(txndata.SpanEvents) { t.Error(txndata.SpanEvents) } } func TestMultipleChildren(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) end2, err2 := endSegment(txndata, thread, t2, start.Add(3*time.Second)) t3 := startSegment(txndata, thread, start.Add(4*time.Second)) end3, err3 := endSegment(txndata, thread, t3, start.Add(5*time.Second)) end1, err1 := endSegment(txndata, thread, t1, start.Add(6*time.Second)) t4 := startSegment(txndata, thread, start.Add(7*time.Second)) end4, err4 := endSegment(txndata, thread, t4, start.Add(8*time.Second)) if nil != err1 || end1.duration != 5*time.Second || end1.exclusive != 3*time.Second { t.Error(end1, err1) } if nil != err2 || end2.duration != end2.exclusive || end2.duration != time.Second { t.Error(end2, err2) } if nil != err3 || end3.duration != end3.exclusive || end3.duration != time.Second { t.Error(end3, err3) } if nil != err4 || end4.duration != end4.exclusive || end4.duration != time.Second { t.Error(end4, err4) } if thread.TotalTime() != 7*time.Second { t.Error(thread.TotalTime()) } } func TestInvalidStart(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} end, err := endSegment(txndata, thread, segmentStartTime{}, start.Add(1*time.Second)) if err != errMalformedSegment { t.Error(end, err) } startSegment(txndata, thread, start.Add(2*time.Second)) end, err = endSegment(txndata, thread, segmentStartTime{}, start.Add(3*time.Second)) if err != errMalformedSegment { t.Error(end, err) } } func TestSegmentAlreadyEnded(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) end, err := endSegment(txndata, thread, t1, start.Add(2*time.Second)) if err != nil { t.Error(end, err) } end, err = endSegment(txndata, thread, t1, start.Add(3*time.Second)) if err != errSegmentOrder { t.Error(end, err) } } func TestSegmentBadStamp(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t1.Stamp++ end, err := endSegment(txndata, thread, t1, start.Add(2*time.Second)) if err != errSegmentOrder { t.Error(end, err) } } func TestSegmentBadDepth(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t1.Depth++ end, err := endSegment(txndata, thread, t1, start.Add(2*time.Second)) if err != errSegmentOrder { t.Error(end, err) } } func TestSegmentNegativeDepth(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t1.Depth = -1 end, err := endSegment(txndata, thread, t1, start.Add(2*time.Second)) if err != errMalformedSegment { t.Error(end, err) } } func TestSegmentOutOfOrder(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) t3 := startSegment(txndata, thread, start.Add(3*time.Second)) end2, err2 := endSegment(txndata, thread, t2, start.Add(4*time.Second)) end3, err3 := endSegment(txndata, thread, t3, start.Add(5*time.Second)) t4 := startSegment(txndata, thread, start.Add(6*time.Second)) end4, err4 := endSegment(txndata, thread, t4, start.Add(7*time.Second)) end1, err1 := endSegment(txndata, thread, t1, start.Add(8*time.Second)) if nil != err1 || end1.duration != 7*time.Second || end1.exclusive != 4*time.Second { t.Error(end1, err1) } if nil != err2 || end2.duration != end2.exclusive || end2.duration != 2*time.Second { t.Error(end2, err2) } if err3 != errSegmentOrder { t.Error(end3, err3) } if nil != err4 || end4.duration != end4.exclusive || end4.duration != 1*time.Second { t.Error(end4, err4) } } // ........................................|-t3-|....|-t4-| // .........................|-t2-|....|-never-finished---------- // ..........|-t1-|....|--never-finished------------------------ // .....|-------alpha------------------------------------------| // 0 1 2 3 4 5 6 7 8 9 10 11 12 func TestLostChildren(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} alpha := startSegment(txndata, thread, start.Add(1*time.Second)) t1 := startSegment(txndata, thread, start.Add(2*time.Second)) endBasicSegment(txndata, thread, t1, start.Add(3*time.Second), "t1") startSegment(txndata, thread, start.Add(4*time.Second)) t2 := startSegment(txndata, thread, start.Add(5*time.Second)) endBasicSegment(txndata, thread, t2, start.Add(6*time.Second), "t2") startSegment(txndata, thread, start.Add(7*time.Second)) t3 := startSegment(txndata, thread, start.Add(8*time.Second)) endBasicSegment(txndata, thread, t3, start.Add(9*time.Second), "t3") t4 := startSegment(txndata, thread, start.Add(10*time.Second)) endBasicSegment(txndata, thread, t4, start.Add(11*time.Second), "t4") endBasicSegment(txndata, thread, alpha, start.Add(12*time.Second), "alpha") metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "Custom/alpha", Scope: "", Forced: false, Data: []float64{1, 11, 7, 11, 11, 121}}, {Name: "Custom/t1", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t2", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t4", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/alpha", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 11, 7, 11, 11, 121}}, {Name: "Custom/t1", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t2", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t4", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) } // ........................................|-t3-|....|-t4-| // .........................|-t2-|....|-never-finished---------- // ..........|-t1-|....|--never-finished------------------------ // |-------root------------------------------------------------- // 0 1 2 3 4 5 6 7 8 9 10 11 12 func TestLostChildrenRoot(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(2*time.Second)) endBasicSegment(txndata, thread, t1, start.Add(3*time.Second), "t1") startSegment(txndata, thread, start.Add(4*time.Second)) t2 := startSegment(txndata, thread, start.Add(5*time.Second)) endBasicSegment(txndata, thread, t2, start.Add(6*time.Second), "t2") startSegment(txndata, thread, start.Add(7*time.Second)) t3 := startSegment(txndata, thread, start.Add(8*time.Second)) endBasicSegment(txndata, thread, t3, start.Add(9*time.Second), "t3") t4 := startSegment(txndata, thread, start.Add(10*time.Second)) endBasicSegment(txndata, thread, t4, start.Add(11*time.Second), "t4") if thread.TotalTime() != 9*time.Second { t.Error(thread.TotalTime()) } metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "Custom/t1", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t2", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t4", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t1", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t2", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t4", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) } func TestNilSpanEvent(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} token := startSegment(txndata, thread, start) stop := start.Add(1 * time.Second) end, err := endSegment(txndata, thread, token, stop) if nil != err { t.Error(err) } // A segment without a SpanId does not create a spanEvent. if evt := end.spanEvent(); evt != nil { t.Error(evt) } } func TestDefaultSpanEvent(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} token := startSegment(txndata, thread, start) stop := start.Add(1 * time.Second) end, err := endSegment(txndata, thread, token, stop) if nil != err { t.Error(err) } end.SpanID = "123" if evt := end.spanEvent(); evt != nil { if evt.GUID != end.SpanID || evt.ParentID != end.ParentID || evt.Timestamp != end.start.Time || evt.Duration != end.duration || evt.IsEntrypoint { t.Error(evt) } } } func TestGetRootSpanID(t *testing.T) { txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } if id := txndata.GetRootSpanID(); id != "1ae969564b34a33e" { t.Error(id) } if id := txndata.GetRootSpanID(); id != "1ae969564b34a33e" { t.Error(id) } } func TestCurrentSpanIdentifier(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } thread := &tracingThread{} id := txndata.CurrentSpanIdentifier(thread) if id != "1ae969564b34a33e" { t.Error(id) } // After starting and ending a segment, the current span id is still the root. t1 := startSegment(txndata, thread, start.Add(1*time.Second)) _, err1 := endSegment(txndata, thread, t1, start.Add(3*time.Second)) if nil != err1 { t.Error(err1) } id = txndata.CurrentSpanIdentifier(thread) if id != "1ae969564b34a33e" { t.Error(id) } // After starting a new segment, there should be a new current span id. startSegment(txndata, thread, start.Add(2*time.Second)) id2 := txndata.CurrentSpanIdentifier(thread) if id2 != "cd1af05fe6923d6d" { t.Error(id2) } } func TestDatastoreSpanAddress(t *testing.T) { if s := datastoreSpanAddress("host", "portPathOrID"); s != "host:portPathOrID" { t.Error(s) } if s := datastoreSpanAddress("host", ""); s != "host" { t.Error(s) } if s := datastoreSpanAddress("", ""); s != "" { t.Error(s) } } func TestSegmentBasic(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) endBasicSegment(txndata, thread, t2, start.Add(3*time.Second), "t2") endBasicSegment(txndata, thread, t1, start.Add(4*time.Second), "t1") t3 := startSegment(txndata, thread, start.Add(5*time.Second)) t4 := startSegment(txndata, thread, start.Add(6*time.Second)) endBasicSegment(txndata, thread, t3, start.Add(7*time.Second), "t3") endBasicSegment(txndata, thread, t4, start.Add(8*time.Second), "out-of-order") t5 := startSegment(txndata, thread, start.Add(9*time.Second)) endBasicSegment(txndata, thread, t5, start.Add(10*time.Second), "t1") metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "Custom/t1", Scope: "", Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "Custom/t2", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: "", Forced: false, Data: []float64{1, 2, 2, 2, 2, 4}}, {Name: "Custom/t1", Scope: txndata.FinalName, Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "Custom/t2", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Custom/t3", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 2, 2, 2, 2, 4}}, }) } func parseURL(raw string) *url.URL { u, _ := url.Parse(raw) return u } func TestSegmentExternal(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t2, Now: start.Add(3 * time.Second), Logger: logger.ShimLogger{}, }) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t1, Now: start.Add(4 * time.Second), URL: parseURL("http://f1.com"), Host: "f1", Logger: logger.ShimLogger{}, }) t3 := startSegment(txndata, thread, start.Add(5*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t3, Now: start.Add(6 * time.Second), URL: parseURL("http://f1.com"), Host: "f1", Logger: logger.ShimLogger{}, }) t4 := startSegment(txndata, thread, start.Add(7*time.Second)) t4.Stamp++ endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t4, Now: start.Add(8 * time.Second), URL: parseURL("http://invalid-token.com"), Host: "invalid-token.com", Logger: logger.ShimLogger{}, }) if txndata.externalCallCount != 3 { t.Error(txndata.externalCallCount) } if txndata.externalDuration != 5*time.Second { t.Error(txndata.externalDuration) } metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "External/allWeb", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "External/f1/all", Scope: "", Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "External/unknown/all", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "External/f1/http", Scope: txndata.FinalName, Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "External/unknown/http", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) metrics = newMetricTable(100, time.Now()) txndata.FinalName = "OtherTransaction/Go/zip" txndata.IsWeb = false mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "External/f1/all", Scope: "", Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "External/unknown/all", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "External/f1/http", Scope: txndata.FinalName, Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "External/unknown/http", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) } func TestSegmentDatastore(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t2, Now: start.Add(3 * time.Second), Product: "MySQL", Operation: "SELECT", Collection: "my_table", }) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t1, Now: start.Add(4 * time.Second), Product: "MySQL", Operation: "SELECT", // missing collection }) t3 := startSegment(txndata, thread, start.Add(5*time.Second)) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t3, Now: start.Add(6 * time.Second), Product: "MySQL", Operation: "SELECT", // missing collection }) t4 := startSegment(txndata, thread, start.Add(7*time.Second)) t4.Stamp++ endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t4, Now: start.Add(8 * time.Second), Product: "MySQL", Operation: "invalid-token", }) t5 := startSegment(txndata, thread, start.Add(9*time.Second)) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t5, Now: start.Add(10 * time.Second), // missing datastore, collection, and operation }) if txndata.datastoreCallCount != 4 { t.Error(txndata.datastoreCallCount) } if txndata.datastoreDuration != 6*time.Second { t.Error(txndata.datastoreDuration) } metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: []float64{4, 6, 5, 1, 3, 12}}, {Name: "Datastore/allWeb", Scope: "", Forced: true, Data: []float64{4, 6, 5, 1, 3, 12}}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/MySQL/allWeb", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/Unknown/all", Scope: "", Forced: true, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/Unknown/allWeb", Scope: "", Forced: true, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/operation/MySQL/SELECT", Scope: "", Forced: false, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/operation/MySQL/SELECT", Scope: txndata.FinalName, Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "Datastore/operation/Unknown/other", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/operation/Unknown/other", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) metrics = newMetricTable(100, time.Now()) txndata.FinalName = "OtherTransaction/Go/zip" txndata.IsWeb = false mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: []float64{4, 6, 5, 1, 3, 12}}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: []float64{4, 6, 5, 1, 3, 12}}, {Name: "Datastore/MySQL/all", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/MySQL/allOther", Scope: "", Forced: true, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/Unknown/all", Scope: "", Forced: true, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/Unknown/allOther", Scope: "", Forced: true, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/operation/MySQL/SELECT", Scope: "", Forced: false, Data: []float64{3, 5, 4, 1, 3, 11}}, {Name: "Datastore/operation/MySQL/SELECT", Scope: txndata.FinalName, Forced: false, Data: []float64{2, 4, 3, 1, 3, 10}}, {Name: "Datastore/operation/Unknown/other", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/operation/Unknown/other", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: "", Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, {Name: "Datastore/statement/MySQL/my_table/SELECT", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 1, 1, 1, 1, 1}}, }) } func TestDatastoreInstancesCrossAgent(t *testing.T) { var testcases []struct { Name string `json:"name"` SystemHostname string `json:"system_hostname"` DBHostname string `json:"db_hostname"` Product string `json:"product"` Port int `json:"port"` Socket string `json:"unix_socket"` DatabasePath string `json:"database_path"` ExpectedMetric string `json:"expected_instance_metric"` } err := crossagent.ReadJSON("datastores/datastore_instances.json", &testcases) if err != nil { t.Fatal(err) } start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) for _, tc := range testcases { portPathOrID := "" if 0 != tc.Port { portPathOrID = strconv.Itoa(tc.Port) } else if "" != tc.Socket { portPathOrID = tc.Socket } else if "" != tc.DatabasePath { portPathOrID = tc.DatabasePath // These tests makes weird assumptions. tc.DBHostname = "localhost" } txndata := &txnData{} thread := &tracingThread{} host := "this-hostname" s := startSegment(txndata, thread, start) endDatastoreSegment(endDatastoreParams{ Thread: thread, TxnData: txndata, Start: s, Now: start.Add(1 * time.Second), Product: tc.Product, Operation: "SELECT", Collection: "my_table", PortPathOrID: portPathOrID, Host: tc.DBHostname, ThisHost: host, }) expect := strings.Replace(tc.ExpectedMetric, tc.SystemHostname, host, -1) metrics := newMetricTable(100, time.Now()) txndata.FinalName = "OtherTransaction/Go/zip" txndata.IsWeb = false mergeBreakdownMetrics(txndata, metrics) data := []float64{1, 1, 1, 1, 1, 1} expectMetrics(extendValidator(t, tc.Name), metrics, []internal.WantMetric{ {Name: "Datastore/all", Scope: "", Forced: true, Data: data}, {Name: "Datastore/allOther", Scope: "", Forced: true, Data: data}, {Name: "Datastore/" + tc.Product + "/all", Scope: "", Forced: true, Data: data}, {Name: "Datastore/" + tc.Product + "/allOther", Scope: "", Forced: true, Data: data}, {Name: "Datastore/operation/" + tc.Product + "/SELECT", Scope: "", Forced: false, Data: data}, {Name: "Datastore/statement/" + tc.Product + "/my_table/SELECT", Scope: "", Forced: false, Data: data}, {Name: "Datastore/statement/" + tc.Product + "/my_table/SELECT", Scope: txndata.FinalName, Forced: false, Data: data}, {Name: expect, Scope: "", Forced: false, Data: data}, }) } } func TestGenericSpanEventCreation(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), ShouldCollectSpanEvents: trueFunc, ShouldCreateSpanGUID: trueFunc, } thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) endBasicSegment(txndata, thread, t1, start.Add(3*time.Second), "t1") // Since a basic segment has just ended, there should be exactly one generic span event in txndata.SpanEvents[] if 1 != len(txndata.SpanEvents) { t.Error(txndata.SpanEvents) } if txndata.SpanEvents[0].Category != spanCategoryGeneric { t.Error(txndata.SpanEvents[0].Category) } } func TestSpanEventNotCollected(t *testing.T) { // Test the situation where ShouldCollectSpanEvents is populated but returns // false. start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), ShouldCollectSpanEvents: falseFunc, ShouldCreateSpanGUID: falseFunc, } thread := &tracingThread{} t1 := startSegment(txndata, thread, start.Add(1*time.Second)) endBasicSegment(txndata, thread, t1, start.Add(3*time.Second), "t1") if 0 != len(txndata.SpanEvents) { t.Error(txndata.SpanEvents) } } func TestDatastoreSpanEventCreation(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } thread := &tracingThread{} // Enable that which is necessary to generate span events when segments are ended. txndata.ShouldCollectSpanEvents = trueFunc txndata.ShouldCreateSpanGUID = trueFunc t1 := startSegment(txndata, thread, start.Add(1*time.Second)) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t1, Now: start.Add(3 * time.Second), Product: "MySQL", Operation: "SELECT", Collection: "my_table", }) // Since a datastore segment has just ended, there should be exactly one datastore span event in txndata.SpanEvents[] if 1 != len(txndata.SpanEvents) { t.Error(txndata.SpanEvents) } if txndata.SpanEvents[0].Category != spanCategoryDatastore { t.Error(txndata.SpanEvents[0].Category) } } func TestHTTPSpanEventCreation(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } thread := &tracingThread{} // Enable that which is necessary to generate span events when segments are ended. txndata.ShouldCollectSpanEvents = trueFunc txndata.ShouldCreateSpanGUID = trueFunc t1 := startSegment(txndata, thread, start.Add(1*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t1, Now: start.Add(3 * time.Second), URL: nil, Logger: logger.ShimLogger{}, }) // Since an external segment has just ended, there should be exactly one HTTP span event in txndata.SpanEvents[] if 1 != len(txndata.SpanEvents) { t.Error(txndata.SpanEvents) } if txndata.SpanEvents[0].Category != spanCategoryHTTP { t.Error(txndata.SpanEvents[0].Category) } } func TestExternalSegmentCAT(t *testing.T) { // Test that when the reading the response CAT headers fails, an external // segment is still created. start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } txndata.CrossProcess.Enabled = true thread := &tracingThread{} resp := &http.Response{Header: http.Header{}} resp.Header.Add(cat.NewRelicAppDataName, "bad header value") t1 := startSegment(txndata, thread, start.Add(1*time.Second)) err := endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t1, Now: start.Add(4 * time.Second), URL: parseURL("http://f1.com"), Logger: logger.ShimLogger{}, }) if nil != err { t.Error("endExternalSegment returned an err:", err) } if txndata.externalCallCount != 1 { t.Error(txndata.externalCallCount) } if txndata.externalDuration != 3*time.Second { t.Error(txndata.externalDuration) } metrics := newMetricTable(100, time.Now()) txndata.FinalName = "OtherTransaction/Go/zip" txndata.IsWeb = false mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "External/all", Scope: "", Forced: true, Data: []float64{1, 3, 3, 3, 3, 9}}, {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{1, 3, 3, 3, 3, 9}}, {Name: "External/f1.com/all", Scope: "", Forced: false, Data: []float64{1, 3, 3, 3, 3, 9}}, {Name: "External/f1.com/http", Scope: txndata.FinalName, Forced: false, Data: []float64{1, 3, 3, 3, 3, 9}}, }) } func TestEndMessageSegment(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } txndata.CrossProcess.Enabled = true thread := &tracingThread{} seg1 := startSegment(txndata, thread, start.Add(1*time.Second)) seg2 := startSegment(txndata, thread, start.Add(2*time.Second)) endMessageSegment(endMessageParams{ TxnData: txndata, Thread: thread, Start: seg1, Now: start.Add(3 * time.Second), Logger: nil, DestinationName: "MyTopic", Library: "Kafka", DestinationType: "Topic", }) endMessageSegment(endMessageParams{ TxnData: txndata, Thread: thread, Start: seg2, Now: start.Add(4 * time.Second), Logger: nil, DestinationName: "MyOtherTopic", Library: "Kafka", DestinationType: "Topic", }) metrics := newMetricTable(100, time.Now()) txndata.FinalName = "WebTransaction/Go/zip" txndata.IsWeb = true mergeBreakdownMetrics(txndata, metrics) expectMetrics(t, metrics, []internal.WantMetric{ {Name: "MessageBroker/Kafka/Topic/Produce/Named/MyTopic", Scope: "WebTransaction/Go/zip", Forced: false, Data: []float64{1, 2, 2, 2, 2, 4}}, {Name: "MessageBroker/Kafka/Topic/Produce/Named/MyTopic", Scope: "", Forced: false, Data: []float64{1, 2, 2, 2, 2, 4}}, }) } func TestBetterCAT_SetTraceAndTxnIDs(t *testing.T) { cases := map[string]string{ "12345678901234567890123456789012": "1234567890123456", "12345678901234567890": "1234567890123456", "1234567890123456": "1234567890123456", "": "", "123456": "123456", } for k, v := range cases { bc := betterCAT{} bc.SetTraceAndTxnIDs(k) if bc.TxnID != v { t.Errorf("Unexpected txn ID - for key %s got %s, but expected %s", k, bc.TxnID, v) } } } func Test_truncateSpanAttribute(t *testing.T) { tests := []struct { name string // description of this test case // Named input parameters for target function. value string maxLengthWithEllipsis int want string }{ { name: "Length of value is less than maxLengthWithEllipsis", value: "SELECT * FROM table", maxLengthWithEllipsis: 25, want: "SELECT * FROM table", }, { name: "Length of value is equal to maxLengthWithEllipsis", value: "SELECT * FROM table WHERE", maxLengthWithEllipsis: 25, want: "SELECT * FROM table WHERE", }, { name: "Length of value is less than maxLengthWithEllipsis with character larger than a byte", value: "SELECT * FROM tablé", maxLengthWithEllipsis: 25, want: "SELECT * FROM tablé", }, { name: "Length of value is equal than maxLengthWithEllipsis with character larger than a byte", value: "SELECT * FROM tablé WHERE", maxLengthWithEllipsis: 25, want: "SELECT * FROM tablé W...", }, { name: "Length of value is longer than maxLengthWithEllipsis", value: "SELECT * FROM table WHERE condition=truncated", maxLengthWithEllipsis: 25, want: "SELECT * FROM table WH...", }, { name: "Length of value is longer than maxLengthWithEllipsis with character larger than a byte", value: "SELECT * FROM tablé WHERE condition=truncated", maxLengthWithEllipsis: 25, want: "SELECT * FROM tablé W...", }, { name: "Length of value is longer than maxLengthWithEllipsis with character larger than a byte in section that gets truncated", value: "SELECT * FROM tablé WHERE condition=truncatéd", maxLengthWithEllipsis: 25, want: "SELECT * FROM tablé W...", }, { name: "Length of value is less than maxLengthWithEllipsis and all characters larger than a byte", value: "éééé", maxLengthWithEllipsis: 5, want: "é...", }, { name: "Length of value is equal to maxLengthWithEllipsis and all characters larger than a byte", value: "ééééé", maxLengthWithEllipsis: 5, want: "é...", }, { name: "Length of value is longer than maxLengthWithEllipsis and all characters larger than a byte", value: "éééééé", maxLengthWithEllipsis: 5, want: "é...", }, { name: "Length of value is longer than maxLengthWithEllipsis and all characters larger than a byte with an even maxLength", value: "ééééééé", maxLengthWithEllipsis: 6, want: "é...", }, { name: "Length of value is shorter than maxLengthWithEllipsis and all characters larger than a byte and an even maxLength", value: "éé", maxLengthWithEllipsis: 6, want: "éé", }, { name: "Length of value is equal to maxLengthWithEllipsis and all characters larger than a byte and an even maxLength", value: "ééé", maxLengthWithEllipsis: 6, want: "ééé", }, { name: "Length of value is longer than maxLengthWithEllipsis and all characters larger than a byte and a larger even maxLength", value: "ééééééééé", maxLengthWithEllipsis: 8, want: "éé...", }, { name: "Length of value is equal to maxLengthWithEllipsis and all characters larger than a byte and a larger even maxLength", value: "éééé", maxLengthWithEllipsis: 8, want: "éééé", }, { name: "Length of value is shorter than maxLengthWithEllipsis and all characters larger two bytes with an odd maxLength", value: "中中", maxLengthWithEllipsis: 7, want: "中中", }, { name: "Length of value is greater than maxLengthWithEllipsis and all characters larger two bytes with an odd maxLength", value: "中中中", maxLengthWithEllipsis: 7, want: "中...", }, { name: "Length of value is equal to maxLengthWithEllipsis and all characters larger two bytes with an even maxLength", value: "中中", maxLengthWithEllipsis: 6, want: "中中", }, { name: "Length of value is larger than maxLengthWithEllipsis and all characters larger two bytes with an even maxLength", value: "中中中", maxLengthWithEllipsis: 6, want: "中...", }, { name: "Length of value is larger than maxLengthWithEllipsis and all characters larger two bytes with an larger odd maxLength", value: "中中中中", maxLengthWithEllipsis: 11, want: "中中...", }, { name: "Length of value is equal to maxLengthWithEllipsis and all characters larger two bytes with a larger odd maxLength", value: "中中中", maxLengthWithEllipsis: 9, want: "中中中", }, { name: "Length of value is shorter than maxLengthWithEllipsis and all characters larger two bytes with an even maxLength", value: "中", maxLengthWithEllipsis: 6, want: "中", }, { name: "Length of value is shorter than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an even maxLength", value: "ééé中", maxLengthWithEllipsis: 10, want: "ééé中", }, { name: "Length of value is shorter than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an odd maxLength", value: "ééé中", maxLengthWithEllipsis: 11, want: "ééé中", }, { name: "Length of value is equal than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an odd maxLength", value: "ééé中", maxLengthWithEllipsis: 9, want: "ééé中", }, { name: "Length of value is equal than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an even maxLength", value: "é中éé中", maxLengthWithEllipsis: 12, want: "é中éé中", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an even maxLength", value: "ééé中", maxLengthWithEllipsis: 8, want: "éé...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an even maxLength but larger byte comes earlier in string", value: "é中éé中", maxLengthWithEllipsis: 8, want: "é中...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an odd maxLength", value: "ééé中é", maxLengthWithEllipsis: 9, want: "ééé...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an odd maxLength but larger character in third position", value: "éé中éé", maxLengthWithEllipsis: 9, want: "éé...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters of one and two bytes and an odd maxLength but larger character in third position", value: "é中ééé", maxLengthWithEllipsis: 9, want: "é中...", }, { name: "Length of value is shorter than maxLengthWithEllipsis and a mixture of characters up to two bytes and an even maxLength", value: "ééd中", maxLengthWithEllipsis: 10, want: "ééd中", }, { name: "Length of value is shorter than maxLengthWithEllipsis and a mixture of characters up to two bytes and an odd maxLength", value: "ééd中", maxLengthWithEllipsis: 9, want: "ééd中", }, { name: "Length of value is equal than maxLengthWithEllipsis and a mixture of characters up to two bytes and an even maxLength", value: "ééd中é", maxLengthWithEllipsis: 10, want: "ééd中é", }, { name: "Length of value is equal than maxLengthWithEllipsis and a mixture of characters up to two bytes and an odd maxLength", value: "ééd中d", maxLengthWithEllipsis: 9, want: "ééd中d", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters up to two bytes and an even maxLength", value: "ééd中é", maxLengthWithEllipsis: 8, want: "ééd...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters up to two bytes and an even maxLength and the single byte character position later", value: "éé中dé", maxLengthWithEllipsis: 8, want: "éé...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters up to two bytes and an odd maxLength", value: "ééd中é", maxLengthWithEllipsis: 9, want: "ééd...", }, { name: "Length of value is longer than maxLengthWithEllipsis and a mixture of characters up to two bytes and an odd maxLength and an extra single byte character added in", value: "éédd中é", maxLengthWithEllipsis: 9, want: "éédd...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := truncateSpanAttribute(tt.value, tt.maxLengthWithEllipsis) if got != tt.want { t.Errorf("truncateSpanAttribute() = %v, want %v\nsize got: %d, want: %d\n", got, tt.want, len(got), len(tt.want)) } }) } } func Test_spanAttributeMap_addString(t *testing.T) { tests := []struct { name string // description of this test case // Named input parameters for target function. key string val string want string }{ { name: "Add a string that is not of key db.statement", key: SpanAttributeDBInstance, val: "DbInstance", want: "DbInstance", }, { name: "Add a different string that is not of key db.statement", key: SpanAttributeAWSRegion, val: "AwsRegion", want: "AwsRegion", }, { name: "Add a string that is db.statement", key: SpanAttributeDBStatement, val: "SELECT * FROM TABLE", want: "SELECT * FROM TABLE", }, { name: "Pass an empty string that is not of key db.statement and don't add to map", key: SpanAttributeAWSOperation, val: "", want: "", // used to check for nil case }, { name: "Pass an empty string that is of key db.statement and don't add to map", key: SpanAttributeDBStatement, val: "", want: "", // used to check for nil case }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var m spanAttributeMap m.addString(tt.key, tt.val) got := m[tt.key] if got != nil && got != stringJSONWriter(tt.want) { t.Errorf("addString() sets m[%v] = %v, want: %v", tt.key, got, tt.want) } else if got == nil && tt.want != "" { t.Errorf("addString() sets m[%v] = %v, want: nil", tt.key, got) } delete(m, tt.key) // clean up key after to prevent testing old cases }) } } go-agent-3.42.0/v3/newrelic/transaction.go000066400000000000000000000600701510742411500203200ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // Transaction instruments one logical unit of work: either an inbound web // request or background task. Start a new Transaction with the // Application.StartTransaction method. // // All methods on Transaction are nil safe. Therefore, a nil Transaction // pointer can be safely used as a mock. type Transaction struct { Private any thread *thread } // nilTransaction guards against nil errors when handling a transaction. func nilTransaction(txn *Transaction) bool { return txn == nil || txn.thread == nil || txn.thread.txn == nil } // End finishes the Transaction. After that, subsequent calls to End or // other Transaction methods have no effect. All segments and // instrumentation must be completed before End is called. func (txn *Transaction) End() { if nilTransaction(txn) { return } var r any if txn.thread.Config.ErrorCollector.RecordPanics { // recover must be called in the function directly being deferred, // not any nested call! r = recover() if nil != r && IsSecurityAgentPresent() { secureAgent.SendEvent("RECORD_PANICS", r) } } if txn.thread.IsWeb && IsSecurityAgentPresent() { secureAgent.SendEvent("INBOUND_END", txn.GetLinkingMetadata().TraceID) } txn.thread.logAPIError(txn.thread.End(r), "end transaction", nil) } // SetOption allows the setting of some transaction TraceOption parameters // after the transaction has already been started, such as specifying a new // source code location for code-level metrics. // // The set of options should be the complete set you wish to have in effect, // just as if you were calling StartTransaction now with the same set of options. func (txn *Transaction) SetOption(options ...TraceOption) { if nilTransaction(txn) { return } txn.thread.txn.setOption(options...) } // Ignore prevents this transaction's data from being recorded. func (txn *Transaction) Ignore() { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.Ignore(), "ignore transaction", nil) } // IgnoreApdex prevents Apdex from being calculated for this transaction. func (txn *Transaction) IgnoreApdex() { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.IgnoreApdex(), "ignore apdex", nil) } // SetName names the transaction. Use a limited set of unique names to // ensure that Transactions are grouped usefully. func (txn *Transaction) SetName(name string) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.SetName(name), "set transaction name", nil) } // Name returns the name currently set for the transaction, as, e.g. by a call to SetName. // If unable to do so (such as due to a nil transaction pointer), the empty string is returned. func (txn *Transaction) Name() string { // This is called Name rather than GetName to be consistent with the prevailing naming // conventions for the Go language, even though the underlying internal call must be called // something else (like GetName) because there's already a Name struct member. if nilTransaction(txn) { return "" } return txn.thread.GetName() } // NoticeError records an error. The Transaction saves the first five // errors. For more control over the recorded error fields, see the // newrelic.Error type. // // In certain situations, using this method may result in an error being // recorded twice. Errors are automatically recorded when // Transaction.WriteHeader receives a status code at or above 400 or strictly // below 100 that is not in the IgnoreStatusCodes configuration list. This // method is unaffected by the IgnoreStatusCodes configuration list. // // NoticeError examines whether the error implements the following optional // methods: // // // StackTrace records a stack trace // StackTrace() []uintptr // // // ErrorClass sets the error's class // ErrorClass() string // // // ErrorAttributes sets the errors attributes // ErrorAttributes() map[string]any // // The newrelic.Error type, which implements these methods, is the recommended // way to directly control the recorded error's message, class, stacktrace, // and attributes. func (txn *Transaction) NoticeError(err error) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.NoticeError(err, false), "notice error", nil) } // NoticeExpectedError records an error that was expected to occur. Errors recoreded with this // method will not trigger any error alerts or count towards your error metrics. // The Transaction saves the first five errors. // For more control over the recorded error fields, see the // newrelic.Error type. // // In certain situations, using this method may result in an error being // recorded twice. Errors are automatically recorded when // Transaction.WriteHeader receives a status code at or above 400 or strictly // below 100 that is not in the IgnoreStatusCodes configuration list. This // method is unaffected by the IgnoreStatusCodes configuration list. // // NoticeExpectedError examines whether the error implements the following optional // methods: // // // StackTrace records a stack trace // StackTrace() []uintptr // // // ErrorClass sets the error's class // ErrorClass() string // // // ErrorAttributes sets the errors attributes // ErrorAttributes() map[string]any // // The newrelic.Error type, which implements these methods, is the recommended // way to directly control the recorded error's message, class, stacktrace, // and attributes. func (txn *Transaction) NoticeExpectedError(err error) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.NoticeError(err, true), "notice error", nil) } // AddAttribute adds a key value pair to the transaction event, errors, // and traces. // // The key must contain fewer than than 255 bytes. The value must be a // number, string, or boolean. // // For more information, see: // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/collect-custom-attributes func (txn *Transaction) AddAttribute(key string, value any) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.AddAttribute(key, value), "add attribute", nil) } // SetUserID is used to track the user that a transaction, and all data that is recorded as a subset of that transaction, // belong to or interact with. This will propogate an attribute containing this information to all events that are // a child of this transaction, like errors and spans. func (txn *Transaction) SetUserID(userID string) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.AddUserID(userID), "set user ID", nil) } // RecordLog records the data from a single log line. // This consumes a LogData object that should be configured // with data taken from a logging framework. // // Certian parts of this feature can be turned off based on your // config settings. Record log is capable of recording log events, // as well as log metrics depending on how your application is // configured. func (txn *Transaction) RecordLog(log LogData) { if nilTransaction(txn) { return } event, err := log.toLogEvent() if err != nil { txn.Application().app.Error("unable to record log", map[string]any{ "reason": err.Error(), }) return } metadata := txn.GetTraceMetadata() event.spanID = metadata.SpanID event.traceID = metadata.TraceID txn.thread.StoreLog(&event) } // SetWebRequestHTTP marks the transaction as a web transaction. If // the request is non-nil, SetWebRequestHTTP will additionally collect // details on request attributes, url, and method. If headers are // present, the agent will look for distributed tracing headers using // Transaction.AcceptDistributedTraceHeaders. func (txn *Transaction) SetWebRequestHTTP(r *http.Request) { if nilTransaction(txn) { return } if r == nil { txn.SetWebRequest(WebRequest{}) return } wr := WebRequest{ Header: r.Header, URL: r.URL, Method: r.Method, Transport: transport(r), Host: r.Host, Body: reqBody(r), ServerName: serverName(r), Type: "HTTP", RemoteAddress: r.RemoteAddr, } txn.SetWebRequest(wr) } // IsEnded returns transaction end status. // If the transaction is nil, the thread is nil, or the transaction is finished, it returns true. // Otherwise, it returns thread.finished value. func (txn *Transaction) IsEnded() bool { if nilTransaction(txn) { return true } return txn.thread.IsEnded() } func transport(r *http.Request) TransportType { if strings.HasPrefix(r.Proto, "HTTP") { if r.TLS != nil { return TransportHTTPS } return TransportHTTP } return TransportUnknown } func serverName(r *http.Request) string { if strings.HasPrefix(r.Proto, "HTTP") { if r.TLS != nil { return r.TLS.ServerName } } return "" } func reqBody(req *http.Request) *BodyBuffer { if IsSecurityAgentPresent() && req.Body != nil && req.Body != http.NoBody { buf := &BodyBuffer{buf: make([]byte, 0, 100)} tee := io.TeeReader(req.Body, buf) req.Body = io.NopCloser(tee) return buf } return nil } // SetWebRequest marks the transaction as a web transaction. SetWebRequest // additionally collects details on request attributes, url, and method if // these fields are set. If headers are present, the agent will look for // distributed tracing headers using Transaction.AcceptDistributedTraceHeaders. // Use Transaction.SetWebRequestHTTP if you have a *http.Request. func (txn *Transaction) SetWebRequest(r WebRequest) { if nilTransaction(txn) { return } if IsSecurityAgentPresent() { secureAgent.SendEvent("INBOUND", r, txn.GetCsecAttributes(), txn.GetLinkingMetadata().TraceID) } txn.thread.logAPIError(txn.thread.SetWebRequest(r), "set web request", nil) } // SetWebResponse allows the Transaction to instrument response code and // response headers. Use the return value of this method in place of the input // parameter http.ResponseWriter in your instrumentation. // // The returned http.ResponseWriter is safe to use even if the Transaction // receiver is nil or has already been ended. // // The returned http.ResponseWriter implements the combination of // http.CloseNotifier, http.Flusher, http.Hijacker, and io.ReaderFrom // implemented by the input http.ResponseWriter. // // This method is used by WrapHandle, WrapHandleFunc, and most integration // package middlewares. Therefore, you probably want to use this only if you // are writing your own instrumentation middleware. func (txn *Transaction) SetWebResponse(w http.ResponseWriter) http.ResponseWriter { if nilTransaction(txn) { return w } return txn.thread.SetWebResponse(w) } // StartSegmentNow starts timing a segment. The SegmentStartTime returned can // be used as the StartTime field in Segment, DatastoreSegment, or // ExternalSegment. The returned SegmentStartTime is safe to use even when the // Transaction receiver is nil. In this case, the segment will have no effect. func (txn *Transaction) StartSegmentNow() SegmentStartTime { return txn.startSegmentAt(time.Now()) } func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { if nilTransaction(txn) { return SegmentStartTime{} } return txn.thread.startSegmentAt(at) } // StartSegment makes it easy to instrument segments. To time a function, do // the following: // // func timeMe(txn newrelic.Transaction) { // defer txn.StartSegment("timeMe").End() // // ... function code here ... // } // // To time a block of code, do the following: // // segment := txn.StartSegment("myBlock") // // ... code you want to time here ... // segment.End() func (txn *Transaction) StartSegment(name string) *Segment { if IsSecurityAgentPresent() && !nilTransaction(txn) && txn.thread.thread != nil && txn.thread.thread.threadID > 0 { // async segment start secureAgent.SendEvent("NEW_GOROUTINE_LINKER", txn.thread.getCsecData()) } return &Segment{ StartTime: txn.StartSegmentNow(), Name: name, } } // InsertDistributedTraceHeaders adds the Distributed Trace headers used to // link transactions. InsertDistributedTraceHeaders should be called every // time an outbound call is made since the payload contains a timestamp. // // When the Distributed Tracer is enabled, InsertDistributedTraceHeaders will // always insert W3C trace context headers. It also by default inserts the New Relic // distributed tracing header, but can be configured based on the // Config.DistributedTracer.ExcludeNewRelicHeader option. // // StartExternalSegment calls InsertDistributedTraceHeaders, so you don't need // to use it for outbound HTTP calls: Just use StartExternalSegment! func (txn *Transaction) InsertDistributedTraceHeaders(hdrs http.Header) { if nilTransaction(txn) { return } txn.thread.CreateDistributedTracePayload(hdrs) } // AcceptDistributedTraceHeaders links transactions by accepting distributed // trace headers from another transaction. // // Transaction.SetWebRequest and Transaction.SetWebRequestHTTP both call this // method automatically with the request headers. Therefore, this method does // not need to be used for typical HTTP transactions. // // AcceptDistributedTraceHeaders should be used as early in the transaction as // possible. It may not be called after a call to // Transaction.InsertDistributedTraceHeaders. // // AcceptDistributedTraceHeaders first looks for the presence of W3C trace // context headers. Only when those are not found will it look for the New // Relic distributed tracing header. func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http.Header) { if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.AcceptDistributedTraceHeaders(t, hdrs), "accept trace payload", nil) } // AcceptDistributedTraceHeadersFromJSON works just like AcceptDistributedTraceHeaders(), except // that it takes the header data as a JSON string à la DistributedTraceHeadersFromJSON(). Additionally // (unlike AcceptDistributedTraceHeaders()) it returns an error if it was unable to successfully // convert the JSON string to http headers. There is no guarantee that the header data found in JSON // is correct beyond conforming to the expected types and syntax. func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, jsondata string) error { if nilTransaction(txn) { // do no work if txn is nil return nil } hdrs, err := DistributedTraceHeadersFromJSON(jsondata) if err != nil { return err } txn.AcceptDistributedTraceHeaders(t, hdrs) return nil } // DistributedTraceHeadersFromJSON takes a set of distributed trace headers as a JSON-encoded string // and emits a http.Header value suitable for passing on to the // txn.AcceptDistributedTraceHeaders() function. // // This is a convenience function provided for cases where you receive the trace header data // already as a JSON string and want to avoid manually converting that to an http.Header. // It helps facilitate handling of headers passed to your Go application from components written in other // languages which may natively handle these header values as JSON strings. // // For example, given the input string // // `{"traceparent": "frob", "tracestate": "blorfl", "newrelic": "xyzzy"}` // // This will emit an http.Header value with headers "traceparent", "tracestate", and "newrelic". // Specifically: // // http.Header{ // "Traceparent": {"frob"}, // "Tracestate": {"blorfl"}, // "Newrelic": {"xyzzy"}, // } // // The JSON string must be a single object whose values may be strings or arrays of strings. // These are translated directly to http headers with singleton or multiple values. // In the case of multiple string values, these are translated to a multi-value HTTP // header. For example: // // `{"traceparent": "12345", "colors": ["red", "green", "blue"]}` // // which produces // // http.Header{ // "Traceparent": {"12345"}, // "Colors": {"red", "green", "blue"}, // } // // (Note that the HTTP headers are capitalized.) func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err error) { var raw any hdrs = http.Header{} if jsondata == "" { return } err = json.Unmarshal([]byte(jsondata), &raw) if err != nil { return } switch d := raw.(type) { case map[string]any: for k, v := range d { switch hval := v.(type) { case string: hdrs.Set(k, hval) case []any: for _, subval := range hval { switch sval := subval.(type) { case string: hdrs.Add(k, sval) default: err = fmt.Errorf("the JSON object must have only strings or arrays of strings") return } } default: err = fmt.Errorf("the JSON object must have only strings or arrays of strings") return } } default: err = fmt.Errorf("the JSON string must consist of only a single object") return } return } // Application returns the Application which started the transaction. func (txn *Transaction) Application() *Application { if nilTransaction(txn) { return nil } return txn.thread.Application() } // BrowserTimingHeader generates the JavaScript required to enable New // Relic's Browser product. This code should be placed into your pages // as close to the top of the element as possible, but after any // position-sensitive tags (for example, X-UA-Compatible or // charset information). // // This function freezes the transaction name: any calls to SetName() // after BrowserTimingHeader() will be ignored. // // The *BrowserTimingHeader return value will be nil if browser // monitoring is disabled, the application is not connected, or an error // occurred. It is safe to call the pointer's methods if it is nil. func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader { if nilTransaction(txn) { return nil } b, err := txn.thread.BrowserTimingHeader() txn.thread.logAPIError(err, "create browser timing header", nil) return b } // NewGoroutine allows you to use the Transaction in multiple // goroutines. // // Each goroutine must have its own Transaction reference returned by // NewGoroutine. You must call NewGoroutine to get a new Transaction // reference every time you wish to pass the Transaction to another // goroutine. It does not matter if you call this before or after the // other goroutine has started. // // All Transaction methods can be used in any Transaction reference. // The Transaction will end when End() is called in any goroutine. // Note that any segments that end after the transaction ends will not // be reported. func (txn *Transaction) NewGoroutine() *Transaction { if nilTransaction(txn) { return nil } newTxn := txn.thread.NewGoroutine() if IsSecurityAgentPresent() && newTxn.thread != nil { newTxn.thread.setCsecData() } return newTxn } // GetTraceMetadata returns distributed tracing identifiers. Empty // string identifiers are returned if the transaction has finished. func (txn *Transaction) GetTraceMetadata() TraceMetadata { if nilTransaction(txn) { return TraceMetadata{} } return txn.thread.GetTraceMetadata() } // GetLinkingMetadata returns the fields needed to link data to a trace or // entity. func (txn *Transaction) GetLinkingMetadata() LinkingMetadata { if nilTransaction(txn) { return LinkingMetadata{} } return txn.thread.GetLinkingMetadata() } // IsSampled indicates if the Transaction is sampled. A sampled // Transaction records a span event for each segment. Distributed tracing // must be enabled for transactions to be sampled. False is returned if // the Transaction has finished. func (txn *Transaction) IsSampled() bool { if nilTransaction(txn) { return false } return txn.thread.IsSampled() } func (txn *Transaction) GetCsecAttributes() map[string]any { if nilTransaction(txn) { return nil } return txn.thread.getCsecAttributes() } func (txn *Transaction) SetCsecAttributes(key string, value any) { if nilTransaction(txn) { return } txn.thread.setCsecAttributes(key, value) } const ( // DistributedTraceNewRelicHeader is the header used by New Relic agents // for automatic trace payload instrumentation. DistributedTraceNewRelicHeader = "Newrelic" // DistributedTraceW3CTraceStateHeader is one of two headers used by W3C // trace context DistributedTraceW3CTraceStateHeader = "Tracestate" // DistributedTraceW3CTraceParentHeader is one of two headers used by W3C // trace context DistributedTraceW3CTraceParentHeader = "Traceparent" ) // TransportType is used in Transaction.AcceptDistributedTraceHeaders to // represent the type of connection that the trace payload was transported // over. type TransportType string // TransportType names used across New Relic agents: const ( TransportUnknown TransportType = "Unknown" TransportHTTP TransportType = "HTTP" TransportHTTPS TransportType = "HTTPS" TransportKafka TransportType = "Kafka" TransportJMS TransportType = "JMS" TransportIronMQ TransportType = "IronMQ" TransportAMQP TransportType = "AMQP" TransportQueue TransportType = "Queue" TransportOther TransportType = "Other" ) func (tt TransportType) toString() string { switch tt { case TransportHTTP, TransportHTTPS, TransportKafka, TransportJMS, TransportIronMQ, TransportAMQP, TransportQueue, TransportOther: return string(tt) default: return string(TransportUnknown) } } // WebRequest is used to provide request information to Transaction.SetWebRequest. type WebRequest struct { // Header may be nil if you don't have any headers or don't want to // transform them to http.Header format. Header http.Header // URL may be nil if you don't have a URL or don't want to transform // it to *url.URL. URL *url.URL // Method is the request's method. Method string // If a distributed tracing header is found in the WebRequest.Header, // this TransportType will be used in the distributed tracing metrics. Transport TransportType // This is the value of the `Host` header. Go does not add it to the // http.Header object and so must be passed separately. Host string // The following fields are needed for the secure agent's vulnerability // detection features. Body *BodyBuffer ServerName string Type string RemoteAddress string Router string } func (webrequest WebRequest) GetHeader() http.Header { return webrequest.Header } func (webrequest WebRequest) GetURL() *url.URL { return webrequest.URL } func (webrequest WebRequest) GetMethod() string { return webrequest.Method } func (webrequest WebRequest) GetTransport() string { return webrequest.Transport.toString() } func (webrequest WebRequest) GetHost() string { return webrequest.Host } func (webrequest WebRequest) GetBody() []byte { if webrequest.Body == nil { return make([]byte, 0) } return webrequest.Body.read() } func (webrequest WebRequest) IsDataTruncated() bool { if webrequest.Body == nil { return false } return webrequest.Body.isBodyTruncated() } func (webrequest WebRequest) GetServerName() string { return webrequest.ServerName } func (webrequest WebRequest) Type1() string { return webrequest.Type } func (webrequest WebRequest) GetRemoteAddress() string { return webrequest.RemoteAddress } // LinkingMetadata is returned by Transaction.GetLinkingMetadata. It contains // identifiers needed to link data to a trace or entity. type LinkingMetadata struct { // TraceID identifies the entire distributed trace. This field is empty // if distributed tracing is disabled. TraceID string // SpanID identifies the currently active segment. This field is empty // if distributed tracing is disabled or the transaction is not sampled. SpanID string // EntityName is the Application name as set on the Config. If multiple // application names are specified in the Config, only the first is // returned. EntityName string // EntityType is the type of this entity and is always the string // "SERVICE". EntityType string // EntityGUID is the unique identifier for this entity. EntityGUID string // Hostname is the hostname this entity is running on. Hostname string } // TraceMetadata is returned by Transaction.GetTraceMetadata. It contains // distributed tracing identifiers. type TraceMetadata struct { // TraceID identifies the entire distributed trace. This field is empty // if distributed tracing is disabled. TraceID string // SpanID identifies the currently active segment. This field is empty // if distributed tracing is disabled or the transaction is not sampled. SpanID string } go-agent-3.42.0/v3/newrelic/transaction_test.go000066400000000000000000000052401510742411500213550ustar00rootroot00000000000000package newrelic import ( "fmt" "net/http" "testing" ) func TestIsEnded(t *testing.T) { tests := []struct { name string txn *Transaction expected bool }{ {"txn is nil", nil, true}, {"thread is nil", &Transaction{thread: nil}, true}, {"txn.thread.txn is nil", &Transaction{thread: &thread{}}, true}, {"txn.thread.txn.finished is true", &Transaction{thread: &thread{txn: &txn{finished: true}}}, true}, {"txn.thread.txn.finished is false", &Transaction{thread: &thread{txn: &txn{finished: false}}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.txn.IsEnded() if result != tt.expected { t.Errorf("IsEnded() = %v; want %v", result, tt.expected) } }) } } func TestTransaction_MethodsWithNilTransaction(t *testing.T) { var nilTxn *Transaction defer func() { if r := recover(); r != nil { t.Errorf("panics should not occur on methods of Transaction: %v", r) } }() // Ensure no panic occurs when calling methods on a nil transaction nilTxn.End() nilTxn.SetOption() nilTxn.Ignore() nilTxn.IgnoreApdex() nilTxn.SetName("test") name := nilTxn.Name() if name != "" { t.Errorf("expected empty string, got %s", name) } nilTxn.NoticeError(fmt.Errorf("test error")) nilTxn.NoticeExpectedError(fmt.Errorf("test expected error")) nilTxn.AddAttribute("key", "value") nilTxn.SetUserID("user123") nilTxn.RecordLog(LogData{}) nilTxn.SetWebRequestHTTP(nil) nilTxn.SetWebRequest(WebRequest{}) nilTxn.SetWebResponse(nil) nilTxn.StartSegmentNow() nilTxn.StartSegment("test segment") nilTxn.InsertDistributedTraceHeaders(http.Header{}) nilTxn.AcceptDistributedTraceHeaders(TransportHTTP, http.Header{}) err := nilTxn.AcceptDistributedTraceHeadersFromJSON(TransportHTTP, "{}") if err != nil { t.Errorf("expected no error, got %v", err) } app := nilTxn.Application() if app != nil { t.Errorf("expected nil, got %v", app) } bth := nilTxn.BrowserTimingHeader() if bth != nil { t.Errorf("expected nil, got %v", bth) } newTxn := nilTxn.NewGoroutine() if newTxn != nil { t.Errorf("expected nil, got %v", newTxn) } traceMetadata := nilTxn.GetTraceMetadata() if traceMetadata != (TraceMetadata{}) { t.Errorf("expected empty TraceMetadata, got %v", traceMetadata) } linkingMetadata := nilTxn.GetLinkingMetadata() if linkingMetadata != (LinkingMetadata{}) { t.Errorf("expected empty LinkingMetadata, got %v", linkingMetadata) } isSampled := nilTxn.IsSampled() if isSampled { t.Errorf("expected false, got %v", isSampled) } csecAttributes := nilTxn.GetCsecAttributes() if csecAttributes != nil { t.Errorf("expected nil, got %v", csecAttributes) } nilTxn.SetCsecAttributes("key", "value") } go-agent-3.42.0/v3/newrelic/transport.go000066400000000000000000000015371510742411500200320ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build go1.13 // +build go1.13 package newrelic import ( "net" "net/http" "time" ) var ( // collectorDefaultTransport is the http.Transport to be used with // communication to the collector backend if a Transport is not set on the // Config. collectorDefaultTransport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, // added in go 1.13 MaxIdleConns: 100, MaxIdleConnsPerHost: 100, // note: different from default global transport IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } ) go-agent-3.42.0/v3/newrelic/transport_1_12.go000066400000000000000000000014561510742411500205540ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !go1.13 // +build !go1.13 package newrelic import ( "net" "net/http" "time" ) var ( // collectorDefaultTransport is the http.Transport to be used with // communication to the collector backend if a Transport is not set on the // Config. collectorDefaultTransport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns: 100, MaxIdleConnsPerHost: 100, // note: different from default global transport IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } ) go-agent-3.42.0/v3/newrelic/txn_cross_process.go000066400000000000000000000313461510742411500215570ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "errors" "fmt" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" ) // Bitfield values for the txnCrossProcess.Type field. const ( txnCrossProcessSynthetics = (1 << 0) txnCrossProcessInbound = (1 << 1) txnCrossProcessOutbound = (1 << 2) ) var ( // errAccountNotTrusted indicates that, while the inbound headers were valid, // the account ID within them is not trusted by the user's application. errAccountNotTrusted = errors.New("account not trusted") ) // txnCrossProcess contains the metadata required for CAT and Synthetics // headers, transaction events, and traces. type txnCrossProcess struct { // The user side switch controlling whether CAT is enabled or not. Enabled bool // The user side switch controlling whether Distributed Tracing is enabled or not // This is required by synthetics support. If Distributed Tracing is enabled, // any synthetics functionality that is triggered should not set nr.guid. DistributedTracingEnabled bool // Rather than copying in the entire ConnectReply, here are the fields that // we need to support CAT. CrossProcessID []byte EncodingKey []byte TrustedAccounts internal.TrustedAccountSet // CAT state for a given transaction. Type uint8 ClientID string GUID string TripID string PathHash string AlternatePathHashes map[string]bool ReferringPathHash string ReferringTxnGUID string Synthetics *cat.SyntheticsHeader SyntheticsInfo *cat.SyntheticsInfo // The encoded synthetics header received as part of the request headers, if // any. By storing this here, we avoid needing to marshal the invariant // Synthetics struct above each time an external segment is created. SyntheticsHeader string // The encoded synthetics info header received as part of the request headers, if // any. By storing this here, we avoid needing to marshal the invariant // Synthetics struct above each time an external segment is created. SyntheticsInfoHeader string } // crossProcessMetadata represents the metadata that must be transmitted with // an external request for CAT to work. type crossProcessMetadata struct { ID string TxnData string Synthetics string SyntheticsInfo string } // Init initialises a txnCrossProcess based on the given application connect // reply. func (txp *txnCrossProcess) Init(enabled bool, dt bool, reply *internal.ConnectReply) { txp.CrossProcessID = []byte(reply.CrossProcessID) txp.EncodingKey = []byte(reply.EncodingKey) txp.DistributedTracingEnabled = dt txp.Enabled = enabled txp.TrustedAccounts = reply.TrustedAccounts } // CreateCrossProcessMetadata generates request metadata that enable CAT and // Synthetics support for an external segment. func (txp *txnCrossProcess) CreateCrossProcessMetadata(txnName, appName string) (crossProcessMetadata, error) { metadata := crossProcessMetadata{} // Regardless of the user's CAT settings, if there was a synthetics header in // the inbound request, a synthetics header should always be included in the // outbound request headers. if txp.IsSynthetics() { metadata.Synthetics = txp.SyntheticsHeader metadata.SyntheticsInfo = txp.SyntheticsInfoHeader } if txp.Enabled { txp.SetOutbound(true) txp.requireTripID() id, err := txp.outboundID() if err != nil { return metadata, err } txnData, err := txp.outboundTxnData(txnName, appName) if err != nil { return metadata, err } metadata.ID = id metadata.TxnData = txnData } return metadata, nil } // Finalise handles any end-of-transaction tasks. In practice, this simply // means ensuring the path hash is set if it hasn't already been. func (txp *txnCrossProcess) Finalise(txnName, appName string) error { if txp.Enabled && txp.Used() { _, err := txp.setPathHash(txnName, appName) return err } // If there was no CAT activity, then do nothing, successfully. return nil } // IsInbound returns true if the transaction had inbound CAT headers. func (txp *txnCrossProcess) IsInbound() bool { return 0 != (txp.Type & txnCrossProcessInbound) } // IsOutbound returns true if the transaction has generated outbound CAT // headers. func (txp *txnCrossProcess) IsOutbound() bool { // We don't actually use this anywhere today, but it feels weird not having // it. return 0 != (txp.Type & txnCrossProcessOutbound) } // IsSynthetics returns true if the transaction had inbound Synthetics headers. func (txp *txnCrossProcess) IsSynthetics() bool { // Technically, this is redundant: the presence of a non-nil Synthetics // pointer should be sufficient to determine if this is a synthetics // transaction. Nevertheless, it's convenient to have the Type field be // non-zero if any CAT behaviour has occurred. return (txp.Type&txnCrossProcessSynthetics) != 0 && txp.Synthetics != nil } // ParseAppData decodes the given appData value. func (txp *txnCrossProcess) ParseAppData(encodedAppData string) (*cat.AppDataHeader, error) { if !txp.Enabled { return nil, nil } if encodedAppData != "" { rawAppData, err := deobfuscate(encodedAppData, txp.EncodingKey) if err != nil { return nil, err } appData := &cat.AppDataHeader{} if err := json.Unmarshal(rawAppData, appData); err != nil { return nil, err } return appData, nil } return nil, nil } // CreateAppData creates the appData value that should be sent with a response // to ensure CAT operates as expected. func (txp *txnCrossProcess) CreateAppData(name string, queueTime, responseTime time.Duration, contentLength int64) (string, error) { // If CAT is disabled, do nothing, successfully. if !txp.Enabled { return "", nil } data, err := json.Marshal(&cat.AppDataHeader{ CrossProcessID: string(txp.CrossProcessID), TransactionName: name, QueueTimeInSeconds: queueTime.Seconds(), ResponseTimeInSeconds: responseTime.Seconds(), ContentLength: contentLength, TransactionGUID: txp.GUID, }) if err != nil { return "", err } obfuscated, err := obfuscate(data, txp.EncodingKey) if err != nil { return "", err } return obfuscated, nil } // Used returns true if any CAT or Synthetics related functionality has been // triggered on the transaction. func (txp *txnCrossProcess) Used() bool { return 0 != txp.Type } // SetInbound sets the inbound CAT flag. This function is provided only for // internal and unit testing purposes, and should not be used outside of this // package normally. func (txp *txnCrossProcess) SetInbound(inbound bool) { if inbound { txp.Type |= txnCrossProcessInbound } else { txp.Type &^= txnCrossProcessInbound } } // SetOutbound sets the outbound CAT flag. This function is provided only for // internal and unit testing purposes, and should not be used outside of this // package normally. func (txp *txnCrossProcess) SetOutbound(outbound bool) { if outbound { txp.Type |= txnCrossProcessOutbound } else { txp.Type &^= txnCrossProcessOutbound } } // SetSynthetics sets the Synthetics CAT flag. This function is provided only // for internal and unit testing purposes, and should not be used outside of // this package normally. func (txp *txnCrossProcess) SetSynthetics(synthetics bool) { if synthetics { txp.Type |= txnCrossProcessSynthetics } else { txp.Type &^= txnCrossProcessSynthetics } } // handleInboundRequestHeaders parses the CAT headers from the given metadata // and updates the relevant fields on the provided TxnData. func (txp *txnCrossProcess) handleInboundRequestHeaders(metadata crossProcessMetadata) error { if txp.Enabled && metadata.ID != "" && metadata.TxnData != "" { if err := txp.handleInboundRequestEncodedCAT(metadata.ID, metadata.TxnData); err != nil { return err } } if metadata.Synthetics != "" { if err := txp.handleInboundRequestEncodedSynthetics(metadata.Synthetics); err != nil { return err } if metadata.SyntheticsInfo != "" { if err := txp.handleInboundRequestEncodedSyntheticsInfo(metadata.SyntheticsInfo); err != nil { return err } } } return nil } func (txp *txnCrossProcess) handleInboundRequestEncodedCAT(encodedID, encodedTxnData string) error { rawID, err := deobfuscate(encodedID, txp.EncodingKey) if err != nil { return err } rawTxnData, err := deobfuscate(encodedTxnData, txp.EncodingKey) if err != nil { return err } if err := txp.handleInboundRequestID(rawID); err != nil { return err } return txp.handleInboundRequestTxnData(rawTxnData) } func (txp *txnCrossProcess) handleInboundRequestID(raw []byte) error { id, err := cat.NewIDHeader(raw) if err != nil { return err } if !txp.TrustedAccounts.IsTrusted(id.AccountID) { return errAccountNotTrusted } txp.SetInbound(true) txp.ClientID = string(raw) txp.setRequireGUID() return nil } func (txp *txnCrossProcess) handleInboundRequestTxnData(raw []byte) error { txnData := &cat.TxnDataHeader{} if err := json.Unmarshal(raw, txnData); err != nil { return err } txp.SetInbound(true) if txnData.TripID != "" { txp.TripID = txnData.TripID } else { txp.setRequireGUID() txp.TripID = txp.GUID } txp.ReferringTxnGUID = txnData.GUID txp.ReferringPathHash = txnData.PathHash return nil } func (txp *txnCrossProcess) handleInboundRequestEncodedSynthetics(encoded string) error { raw, err := deobfuscate(encoded, txp.EncodingKey) if err != nil { return err } if err := txp.handleInboundRequestSynthetics(raw); err != nil { return err } txp.SyntheticsHeader = encoded return nil } func (txp *txnCrossProcess) handleInboundRequestSynthetics(raw []byte) error { synthetics := &cat.SyntheticsHeader{} if err := json.Unmarshal(raw, synthetics); err != nil { return err } // The specced behaviour here if the account isn't trusted is to disable the // synthetics handling, but not CAT in general, so we won't return an error // here. if txp.TrustedAccounts.IsTrusted(synthetics.AccountID) { txp.SetSynthetics(true) txp.setRequireGUID() txp.Synthetics = synthetics } return nil } func (txp *txnCrossProcess) handleInboundRequestEncodedSyntheticsInfo(encoded string) error { raw, err := deobfuscate(encoded, txp.EncodingKey) if err != nil { return err } if err := txp.handleInboundRequestSyntheticsInfo(raw); err != nil { return err } txp.SyntheticsInfoHeader = encoded return nil } func (txp *txnCrossProcess) handleInboundRequestSyntheticsInfo(raw []byte) error { synthetics := &cat.SyntheticsInfo{} if err := json.Unmarshal(raw, synthetics); err != nil { return err } // The specced behaviour here if the account isn't trusted is to disable the // synthetics handling, but not CAT in general, so we won't return an error // here. if txp.IsSynthetics() { txp.SyntheticsInfo = synthetics } return nil } func (txp *txnCrossProcess) outboundID() (string, error) { return obfuscate(txp.CrossProcessID, txp.EncodingKey) } func (txp *txnCrossProcess) outboundTxnData(txnName, appName string) (string, error) { pathHash, err := txp.setPathHash(txnName, appName) if err != nil { return "", err } data, err := json.Marshal(&cat.TxnDataHeader{ GUID: txp.GUID, TripID: txp.TripID, PathHash: pathHash, }) if err != nil { return "", err } return obfuscate(data, txp.EncodingKey) } // setRequireGUID ensures that the transaction has a valid GUID, and sets the // nr.guid and trip ID if they are not already set. If the customer has enabled // DistributedTracing, then the new style of guid will be set elsewhere. func (txp *txnCrossProcess) setRequireGUID() { if txp.DistributedTracingEnabled { return } if txp.GUID != "" { return } txp.GUID = fmt.Sprintf("%x", randUint64()) if txp.TripID == "" { txp.requireTripID() } } // requireTripID ensures that the transaction has a valid trip ID. func (txp *txnCrossProcess) requireTripID() { if !txp.Enabled { return } if txp.TripID != "" { return } txp.setRequireGUID() txp.TripID = txp.GUID } // setPathHash generates a path hash, sets the transaction's path hash to // match, and returns it. This function will also ensure that the alternate // path hashes are correctly updated. func (txp *txnCrossProcess) setPathHash(txnName, appName string) (string, error) { pathHash, err := cat.GeneratePathHash(txp.ReferringPathHash, txnName, appName) if err != nil { return "", err } if pathHash != txp.PathHash { if txp.PathHash != "" { // Lazily initialise the alternate path hashes if they haven't been // already. if txp.AlternatePathHashes == nil { txp.AlternatePathHashes = make(map[string]bool) } // The spec limits us to a maximum of 10 alternate path hashes. if len(txp.AlternatePathHashes) < 10 { txp.AlternatePathHashes[txp.PathHash] = true } } txp.PathHash = pathHash } return pathHash, nil } go-agent-3.42.0/v3/newrelic/txn_cross_process_test.go000066400000000000000000000616321510742411500226170ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "fmt" "net/http" "reflect" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" ) var ( replyAccountOne = &internal.ConnectReply{ CrossProcessID: "1#1", EncodingKey: "foo", TrustedAccounts: map[int]struct{}{1: {}}, } replyAccountTwo = &internal.ConnectReply{ CrossProcessID: "2#2", EncodingKey: "foo", TrustedAccounts: map[int]struct{}{2: {}}, } requestEmpty = newRequest().Request requestCATOne = newRequest().withCAT(newTxnCrossProcessFromConnectReply(replyAccountOne), "txn", "app").Request requestSyntheticsOne = newRequest().withSynthetics(1, "foo").Request requestCATSyntheticsOne = newRequest().withCAT(newTxnCrossProcessFromConnectReply(replyAccountOne), "txn", "app").withSynthetics(1, "foo").Request ) func mustObfuscate(input, encodingKey string) string { output, err := obfuscate([]byte(input), []byte(encodingKey)) if err != nil { panic(err) } return string(output) } func newTxnCrossProcessFromConnectReply(reply *internal.ConnectReply) *txnCrossProcess { txp := &txnCrossProcess{GUID: "abcdefgh"} txp.Init(true, false, reply) return txp } type request struct { *http.Request } func newRequest() *request { req, err := http.NewRequest("GET", "http://foo.bar", nil) if err != nil { panic(err) } return &request{Request: req} } func (req *request) withCAT(txp *txnCrossProcess, txnName, appName string) *request { metadata, err := txp.CreateCrossProcessMetadata(txnName, appName) if err != nil { panic(err) } for k, values := range metadataToHTTPHeader(metadata) { for _, v := range values { req.Header.Add(k, v) } } return req } func (req *request) withSynthetics(account int, encodingKey string) *request { header := fmt.Sprintf(`[1,%d,"resource","job","monitor"]`, account) obfuscated, err := obfuscate([]byte(header), []byte(encodingKey)) if err != nil { panic(err) } req.Header.Add(cat.NewRelicSyntheticsName, string(obfuscated)) return req.withSyntheticsInfo("cli", "scheduled", encodingKey) } func (req *request) withSyntheticsInfo(initiator, synthType, encodingKey string) *request { header := fmt.Sprintf(`{"version":1,"type":"%s","initiator":"%s"}`, synthType, initiator) obfuscated, err := obfuscate([]byte(header), []byte(encodingKey)) if err != nil { panic(err) } req.Header.Add(cat.NewRelicSyntheticsInfo, string(obfuscated)) return req } func TestTxnCrossProcessInit(t *testing.T) { for _, tc := range []struct { name string enabled bool reply *internal.ConnectReply req *http.Request expected *txnCrossProcess expectedError bool }{ { name: "disabled", enabled: false, reply: replyAccountOne, req: nil, expected: &txnCrossProcess{ CrossProcessID: []byte("1#1"), EncodingKey: []byte("foo"), Enabled: false, TrustedAccounts: map[int]struct{}{1: {}}, }, expectedError: false, }, { name: "normal connect reply without a request", enabled: true, reply: replyAccountOne, req: nil, expected: &txnCrossProcess{ CrossProcessID: []byte("1#1"), EncodingKey: []byte("foo"), Enabled: true, TrustedAccounts: map[int]struct{}{1: {}}, }, expectedError: false, }, { name: "normal connect reply with a request without headers", enabled: true, reply: replyAccountOne, req: requestEmpty, expected: &txnCrossProcess{ CrossProcessID: []byte("1#1"), EncodingKey: []byte("foo"), Enabled: true, TrustedAccounts: map[int]struct{}{1: {}}, }, expectedError: false, }, { name: "normal connect reply with a request with untrusted headers", enabled: true, reply: replyAccountTwo, req: requestCATOne, expected: &txnCrossProcess{ CrossProcessID: []byte("2#2"), EncodingKey: []byte("foo"), Enabled: true, TrustedAccounts: map[int]struct{}{2: {}}, }, expectedError: true, }, { name: "normal connect reply with a request with trusted headers", enabled: true, reply: replyAccountOne, req: requestCATOne, expected: &txnCrossProcess{ CrossProcessID: []byte("1#1"), EncodingKey: []byte("foo"), Enabled: true, TrustedAccounts: map[int]struct{}{1: {}}, }, expectedError: false, }, } { actual := &txnCrossProcess{} id := "" txnData := "" synthetics := "" syntheticsInfo := "" if tc.req != nil { id = tc.req.Header.Get(cat.NewRelicIDName) txnData = tc.req.Header.Get(cat.NewRelicTxnName) synthetics = tc.req.Header.Get(cat.NewRelicSyntheticsName) syntheticsInfo = tc.req.Header.Get(cat.NewRelicSyntheticsInfo) } actual.Init(tc.enabled, false, tc.reply) err := actual.handleInboundRequestHeaders(crossProcessMetadata{id, txnData, synthetics, syntheticsInfo}) if tc.expectedError == false && err != nil { t.Errorf("%s: unexpected error returned from Init: %v", tc.name, err) } else if tc.expectedError && err == nil { t.Errorf("%s: no error returned from Init when one was expected", tc.name) } if !reflect.DeepEqual(actual.EncodingKey, tc.expected.EncodingKey) { t.Errorf("%s: EncodingKey mismatch: expected=%v; got=%v", tc.name, tc.expected.EncodingKey, actual.EncodingKey) } if !reflect.DeepEqual(actual.CrossProcessID, tc.expected.CrossProcessID) { t.Errorf("%s: CrossProcessID mismatch: expected=%v; got=%v", tc.name, tc.expected.CrossProcessID, actual.CrossProcessID) } if !reflect.DeepEqual(actual.TrustedAccounts, tc.expected.TrustedAccounts) { t.Errorf("%s: TrustedAccounts mismatch: expected=%v; got=%v", tc.name, tc.expected.TrustedAccounts, actual.TrustedAccounts) } if actual.Enabled != tc.expected.Enabled { t.Errorf("%s: Enabled mismatch: expected=%v; got=%v", tc.name, tc.expected.Enabled, actual.Enabled) } } } func TestTxnCrossProcessCreateCrossProcessMetadata(t *testing.T) { for _, tc := range []struct { name string enabled bool reply *internal.ConnectReply req *http.Request txnName string appName string expectedError bool expectedMetadata crossProcessMetadata }{ { name: "disabled, no header", enabled: false, reply: replyAccountOne, req: nil, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{}, }, { name: "disabled, header", enabled: false, reply: replyAccountOne, req: requestCATOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{}, }, { name: "disabled, synthetics", enabled: false, reply: replyAccountOne, req: requestSyntheticsOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, { name: "disabled, header, synthetics", enabled: false, reply: replyAccountOne, req: requestCATSyntheticsOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, { name: "enabled, no header, no synthetics", enabled: true, reply: replyAccountOne, req: requestEmpty, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"00000000","b95be233"]`, "foo"), }, }, { name: "enabled, no header, synthetics", enabled: true, reply: replyAccountOne, req: requestSyntheticsOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"00000000","b95be233"]`, "foo"), Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, { name: "enabled, header, no synthetics", enabled: true, reply: replyAccountOne, req: requestCATOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"abcdefgh","cbec2654"]`, "foo"), }, }, { name: "enabled, header, synthetics", enabled: true, reply: replyAccountOne, req: requestCATSyntheticsOne, txnName: "txn", appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"abcdefgh","cbec2654"]`, "foo"), Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, } { txp := &txnCrossProcess{GUID: "00000000"} txp.Init(tc.enabled, false, tc.reply) if nil != tc.req { txp.InboundHTTPRequest(tc.req.Header) } metadata, err := txp.CreateCrossProcessMetadata(tc.txnName, tc.appName) if tc.expectedError == false && err != nil { t.Errorf("%s: unexpected error returned from CreateCrossProcessMetadata: %v", tc.name, err) } else if tc.expectedError && err == nil { t.Errorf("%s: no error returned from CreateCrossProcessMetadata when one was expected", tc.name) } if !reflect.DeepEqual(tc.expectedMetadata, metadata) { t.Errorf("%s: metadata mismatch: expected=%v; got=%v", tc.name, tc.expectedMetadata, metadata) } // Ensure that a path hash was generated if TxnData was created. if metadata.TxnData != "" && txp.PathHash == "" { t.Errorf("%s: no path hash generated", tc.name) } } } func TestTxnCrossProcessCreateCrossProcessMetadataError(t *testing.T) { // Ensure errors bubble back up from deeper within our obfuscation code. // It's likely impossible to get outboundTxnData() to fail, but we can get // outboundID() to fail by having an empty encoding key. txp := &txnCrossProcess{Enabled: true} metadata, err := txp.CreateCrossProcessMetadata("txn", "app") if metadata.ID != "" || metadata.TxnData != "" || metadata.Synthetics != "" { t.Errorf("one or more metadata fields were set unexpectedly; got %v", metadata) } if err == nil { t.Errorf("did not get expected error with an empty encoding key") } // Test the above with Synthetics support to ensure that the Synthetics // payload is still set. txp = &txnCrossProcess{ Enabled: true, Type: txnCrossProcessSynthetics, SyntheticsHeader: "foo", // This won't be actually examined, but can't be nil for the IsSynthetics() // check to pass. Synthetics: &cat.SyntheticsHeader{}, } metadata, err = txp.CreateCrossProcessMetadata("txn", "app") if metadata.ID != "" || metadata.TxnData != "" { t.Errorf("one or more metadata fields were set unexpectedly; got %v", metadata) } if metadata.Synthetics != "foo" { t.Errorf("unexpected synthetics metadata: expected %s; got %s", "foo", metadata.Synthetics) } if err == nil { t.Errorf("did not get expected error with an empty encoding key") } } func TestTxnCrossProcessFinalise(t *testing.T) { // No CAT. txp := &txnCrossProcess{} txp.Init(true, false, replyAccountOne) if err := txp.Finalise("txn", "app"); err != nil { t.Errorf("unexpected error: %v", err) } if txp.PathHash != "" { t.Errorf("unexpected path hash: %s", txp.PathHash) } // CAT, but no path hash. txp = &txnCrossProcess{} txp.Init(true, false, replyAccountOne) txp.InboundHTTPRequest(requestCATOne.Header) if txp.PathHash != "" { t.Errorf("unexpected path hash: %s", txp.PathHash) } if err := txp.Finalise("txn", "app"); err != nil { t.Errorf("unexpected error: %v", err) } if txp.PathHash == "" { t.Error("unexpected lack of path hash") } // CAT, with a path hash. txp = &txnCrossProcess{} txp.Init(true, false, replyAccountOne) txp.InboundHTTPRequest(requestCATOne.Header) txp.CreateCrossProcessMetadata("txn", "app") if txp.PathHash == "" { t.Error("unexpected lack of path hash") } if err := txp.Finalise("txn", "app"); err != nil { t.Errorf("unexpected error: %v", err) } if txp.PathHash == "" { t.Error("unexpected lack of path hash") } } func TestTxnCrossProcessIsInbound(t *testing.T) { for _, tc := range []struct { txpType uint8 expected bool }{ {0, false}, {txnCrossProcessSynthetics, false}, {txnCrossProcessInbound, true}, {txnCrossProcessOutbound, false}, {txnCrossProcessSynthetics | txnCrossProcessInbound, true}, {txnCrossProcessSynthetics | txnCrossProcessOutbound, false}, {txnCrossProcessInbound | txnCrossProcessOutbound, true}, {txnCrossProcessSynthetics | txnCrossProcessInbound | txnCrossProcessOutbound, true}, } { txp := &txnCrossProcess{Type: tc.txpType} actual := txp.IsInbound() if actual != tc.expected { t.Errorf("unexpected IsInbound result for input %d: expected=%v; got=%v", tc.txpType, tc.expected, actual) } } } func TestTxnCrossProcessIsOutbound(t *testing.T) { for _, tc := range []struct { txpType uint8 expected bool }{ {0, false}, {txnCrossProcessSynthetics, false}, {txnCrossProcessInbound, false}, {txnCrossProcessOutbound, true}, {txnCrossProcessSynthetics | txnCrossProcessInbound, false}, {txnCrossProcessSynthetics | txnCrossProcessOutbound, true}, {txnCrossProcessInbound | txnCrossProcessOutbound, true}, {txnCrossProcessSynthetics | txnCrossProcessInbound | txnCrossProcessOutbound, true}, } { txp := &txnCrossProcess{Type: tc.txpType} actual := txp.IsOutbound() if actual != tc.expected { t.Errorf("unexpected IsOutbound result for input %d: expected=%v; got=%v", tc.txpType, tc.expected, actual) } } } func TestTxnCrossProcessIsSynthetics(t *testing.T) { for _, tc := range []struct { txpType uint8 synthetics *cat.SyntheticsHeader expected bool }{ {0, nil, false}, {txnCrossProcessSynthetics, nil, false}, {txnCrossProcessInbound, nil, false}, {txnCrossProcessOutbound, nil, false}, {txnCrossProcessSynthetics | txnCrossProcessInbound, nil, false}, {txnCrossProcessSynthetics | txnCrossProcessOutbound, nil, false}, {txnCrossProcessInbound | txnCrossProcessOutbound, nil, false}, {txnCrossProcessSynthetics | txnCrossProcessInbound | txnCrossProcessOutbound, nil, false}, {0, &cat.SyntheticsHeader{}, false}, {txnCrossProcessSynthetics, &cat.SyntheticsHeader{}, true}, {txnCrossProcessInbound, &cat.SyntheticsHeader{}, false}, {txnCrossProcessOutbound, &cat.SyntheticsHeader{}, false}, {txnCrossProcessSynthetics | txnCrossProcessInbound, &cat.SyntheticsHeader{}, true}, {txnCrossProcessSynthetics | txnCrossProcessOutbound, &cat.SyntheticsHeader{}, true}, {txnCrossProcessInbound | txnCrossProcessOutbound, &cat.SyntheticsHeader{}, false}, {txnCrossProcessSynthetics | txnCrossProcessInbound | txnCrossProcessOutbound, &cat.SyntheticsHeader{}, true}, } { txp := &txnCrossProcess{Type: tc.txpType, Synthetics: tc.synthetics} actual := txp.IsSynthetics() if actual != tc.expected { t.Errorf("unexpected IsSynthetics result for input %d and %p: expected=%v; got=%v", tc.txpType, tc.synthetics, tc.expected, actual) } } } func TestTxnCrossProcessUsed(t *testing.T) { for _, tc := range []struct { txpType uint8 expected bool }{ {0, false}, {txnCrossProcessSynthetics, true}, {txnCrossProcessInbound, true}, {txnCrossProcessOutbound, true}, {txnCrossProcessSynthetics | txnCrossProcessInbound, true}, {txnCrossProcessSynthetics | txnCrossProcessOutbound, true}, {txnCrossProcessInbound | txnCrossProcessOutbound, true}, {txnCrossProcessSynthetics | txnCrossProcessInbound | txnCrossProcessOutbound, true}, } { txp := &txnCrossProcess{Type: tc.txpType} actual := txp.Used() if actual != tc.expected { t.Errorf("unexpected Used result for input %d: expected=%v; got=%v", tc.txpType, tc.expected, actual) } } } func TestTxnCrossProcessSetInbound(t *testing.T) { txp := &txnCrossProcess{Type: 0} txp.SetInbound(false) if txp.IsInbound() != false { t.Error("Inbound is not false after being set to false from false") } txp.SetInbound(true) if txp.IsInbound() != true { t.Error("Inbound is not true after being set to true from false") } txp.SetInbound(true) if txp.IsInbound() != true { t.Error("Inbound is not true after being set to true from true") } txp.SetInbound(false) if txp.IsInbound() != false { t.Error("Inbound is not false after being set to false from true") } } func TestTxnCrossProcessSetOutbound(t *testing.T) { txp := &txnCrossProcess{Type: 0} txp.SetOutbound(false) if txp.IsOutbound() != false { t.Error("Outbound is not false after being set to false from false") } txp.SetOutbound(true) if txp.IsOutbound() != true { t.Error("Outbound is not true after being set to true from false") } txp.SetOutbound(true) if txp.IsOutbound() != true { t.Error("Outbound is not true after being set to true from true") } txp.SetOutbound(false) if txp.IsOutbound() != false { t.Error("Outbound is not false after being set to false from true") } } func TestTxnCrossProcessSetSynthetics(t *testing.T) { // We'll always set SyntheticsHeader, since we're not really testing the full // behaviour of IsSynthetics() here. txp := &txnCrossProcess{ Type: 0, Synthetics: &cat.SyntheticsHeader{}, } txp.SetSynthetics(false) if txp.IsSynthetics() != false { t.Error("Synthetics is not false after being set to false from false") } txp.SetSynthetics(true) if txp.IsSynthetics() != true { t.Error("Synthetics is not true after being set to true from false") } txp.SetSynthetics(true) if txp.IsSynthetics() != true { t.Error("Synthetics is not true after being set to true from true") } txp.SetSynthetics(false) if txp.IsSynthetics() != false { t.Error("Synthetics is not false after being set to false from true") } } func TestTxnCrossProcessParseAppData(t *testing.T) { for _, tc := range []struct { name string encodingKey string input string expectedAppData *cat.AppDataHeader expectedError bool }{ { name: "empty string", encodingKey: "foo", input: "", expectedAppData: nil, expectedError: false, }, { name: "invalidly encoded string", encodingKey: "foo", input: "xxx", expectedAppData: nil, expectedError: true, }, { name: "invalid JSON", encodingKey: "foo", input: mustObfuscate("xxx", "foo"), expectedAppData: nil, expectedError: true, }, { name: "invalid encoding key", encodingKey: "foo", input: mustObfuscate(`["xp","txn",1,2,3,"guid",false]`, "bar"), expectedAppData: nil, expectedError: true, }, { name: "success", encodingKey: "foo", input: mustObfuscate(`["xp","txn",1,2,3,"guid",false]`, "foo"), expectedAppData: &cat.AppDataHeader{ CrossProcessID: "xp", TransactionName: "txn", QueueTimeInSeconds: 1, ResponseTimeInSeconds: 2, ContentLength: 3, TransactionGUID: "guid", }, expectedError: false, }, } { txp := &txnCrossProcess{ Enabled: true, EncodingKey: []byte(tc.encodingKey), } actualAppData, actualErr := txp.ParseAppData(tc.input) if tc.expectedError && actualErr == nil { t.Errorf("%s: expected an error, but didn't get one", tc.name) } else if tc.expectedError == false && actualErr != nil { t.Errorf("%s: expected no error, but got %v", tc.name, actualErr) } if !reflect.DeepEqual(actualAppData, tc.expectedAppData) { t.Errorf("%s: app data mismatched: expected=%v; got=%v", tc.name, tc.expectedAppData, actualAppData) } } } func TestTxnCrossProcessCreateAppData(t *testing.T) { for _, tc := range []struct { name string enabled bool crossProcessID string encodingKey string txnName string queueTime time.Duration responseTime time.Duration contentLength int64 guid string expectedAppData string expectedError bool }{ { name: "cat disabled", enabled: false, crossProcessID: "1#1", encodingKey: "foo", txnName: "txn", queueTime: 1 * time.Second, responseTime: 2 * time.Second, contentLength: 4096, guid: "", expectedAppData: "", expectedError: false, }, { name: "invalid encoding key", enabled: true, crossProcessID: "1#1", encodingKey: "", txnName: "txn", queueTime: 1 * time.Second, responseTime: 2 * time.Second, contentLength: 4096, guid: "", expectedAppData: "", expectedError: true, }, { name: "success", enabled: true, crossProcessID: "1#1", encodingKey: "foo", txnName: "txn", queueTime: 1 * time.Second, responseTime: 2 * time.Second, contentLength: 4096, guid: "guid", expectedAppData: mustObfuscate(`["1#1","txn",1,2,4096,"guid",false]`, "foo"), expectedError: false, }, } { txp := &txnCrossProcess{ Enabled: tc.enabled, EncodingKey: []byte(tc.encodingKey), CrossProcessID: []byte(tc.crossProcessID), GUID: tc.guid, } actualAppData, actualErr := txp.CreateAppData(tc.txnName, tc.queueTime, tc.responseTime, tc.contentLength) if tc.expectedError && actualErr == nil { t.Errorf("%s: expected an error, but didn't get one", tc.name) } else if tc.expectedError == false && actualErr != nil { t.Errorf("%s: expected no error, but got %v", tc.name, actualErr) } if !reflect.DeepEqual(actualAppData, tc.expectedAppData) { t.Errorf("%s: app data mismatched: expected=%v; got=%v", tc.name, tc.expectedAppData, actualAppData) } } } func TestTxnCrossProcessHandleInboundRequestHeaders(t *testing.T) { for _, tc := range []struct { name string enabled bool reply *internal.ConnectReply metadata crossProcessMetadata expectedError bool }{ { name: "disabled, invalid encoding key, invalid synthetics", enabled: false, reply: &internal.ConnectReply{ EncodingKey: "", }, metadata: crossProcessMetadata{ Synthetics: "foo", }, expectedError: true, }, { name: "disabled, valid encoding key, invalid synthetics", enabled: false, reply: replyAccountOne, metadata: crossProcessMetadata{ Synthetics: "foo", }, expectedError: true, }, { name: "disabled, valid encoding key, valid synthetics", enabled: false, reply: replyAccountOne, metadata: crossProcessMetadata{ Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), }, expectedError: false, }, { name: "enabled, invalid encoding key, valid input", enabled: true, reply: &internal.ConnectReply{ EncodingKey: "", }, metadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"00000000","b95be233"]`, "foo"), }, expectedError: true, }, { name: "enabled, valid encoding key, invalid id", enabled: true, reply: replyAccountOne, metadata: crossProcessMetadata{ ID: mustObfuscate(`1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"00000000","b95be233"]`, "foo"), }, expectedError: true, }, { name: "enabled, valid encoding key, invalid txndata", enabled: true, reply: replyAccountOne, metadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",invalid,"00000000","b95be233"]`, "foo"), }, expectedError: true, }, { name: "enabled, valid encoding key, valid input", enabled: true, reply: replyAccountOne, metadata: crossProcessMetadata{ ID: mustObfuscate(`1#1`, "foo"), TxnData: mustObfuscate(`["00000000",false,"00000000","b95be233"]`, "foo"), }, expectedError: false, }, } { txp := &txnCrossProcess{Enabled: tc.enabled} txp.Init(tc.enabled, false, tc.reply) err := txp.handleInboundRequestHeaders(tc.metadata) if tc.expectedError && err == nil { t.Errorf("%s: expected error, but didn't get one", tc.name) } else if tc.expectedError == false && err != nil { t.Errorf("%s: expected no error, but got %v", tc.name, err) } } } go-agent-3.42.0/v3/newrelic/txn_events.go000066400000000000000000000135761510742411500202010ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "fmt" "sort" "strings" "time" ) // WriteJSON prepares JSON in the format expected by the collector. func (e *txnEvent) WriteJSON(buf *bytes.Buffer) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('[') buf.WriteByte('{') w.stringField("type", "Transaction") w.stringField("name", e.FinalName) w.intField("timestamp", timeToIntMillis(e.Start)) if apdexNone != e.Zone { w.stringField("nr.apdexPerfZone", e.Zone.label()) } w.boolField("error", e.HasError) sharedTransactionIntrinsics(e, &w) // totalTime gets put into transaction events but not error events: // https://source.datanerd.us/agents/agent-specs/blob/master/Total-Time-Async.md#attributes w.floatField("totalTime", e.TotalTime.Seconds()) // Write better CAT intrinsics if enabled sharedBetterCATIntrinsics(e, &w) if e.BetterCAT.Enabled { if p := e.BetterCAT.Inbound; nil != p { if "" != p.TransactionID { w.stringField("parentId", p.TransactionID) } if "" != p.ID { w.stringField("parentSpanId", p.ID) } } } // Write old CAT intrinsics if enabled oldCATIntrinsics(e, &w) buf.WriteByte('}') buf.WriteByte(',') userAttributesJSON(e.Attrs, buf, destTxnEvent, nil) buf.WriteByte(',') agentAttributesJSON(e.Attrs, buf, destTxnEvent) buf.WriteByte(']') } // oldCATIntrinsics reports old CAT intrinsics for Transaction // if CrossProcess.Used() is true func oldCATIntrinsics(e *txnEvent, w *jsonFieldsWriter) { if !e.CrossProcess.Used() { return } if e.CrossProcess.ClientID != "" { w.stringField("client_cross_process_id", e.CrossProcess.ClientID) } if e.CrossProcess.TripID != "" { w.stringField("nr.tripId", e.CrossProcess.TripID) } if e.CrossProcess.PathHash != "" { w.stringField("nr.pathHash", e.CrossProcess.PathHash) } if e.CrossProcess.ReferringPathHash != "" { w.stringField("nr.referringPathHash", e.CrossProcess.ReferringPathHash) } if e.CrossProcess.GUID != "" { w.stringField("nr.guid", e.CrossProcess.GUID) } if e.CrossProcess.ReferringTxnGUID != "" { w.stringField("nr.referringTransactionGuid", e.CrossProcess.ReferringTxnGUID) } if len(e.CrossProcess.AlternatePathHashes) > 0 { hashes := make([]string, 0, len(e.CrossProcess.AlternatePathHashes)) for hash := range e.CrossProcess.AlternatePathHashes { hashes = append(hashes, hash) } sort.Strings(hashes) w.stringField("nr.alternatePathHashes", strings.Join(hashes, ",")) } } // sharedTransactionIntrinsics reports intrinsics that are shared // by Transaction and TransactionError func sharedTransactionIntrinsics(e *txnEvent, w *jsonFieldsWriter) { w.floatField("duration", e.Duration.Seconds()) if e.Queuing > 0 { w.floatField("queueDuration", e.Queuing.Seconds()) } if e.externalCallCount > 0 { w.intField("externalCallCount", int64(e.externalCallCount)) w.floatField("externalDuration", e.externalDuration.Seconds()) } if e.datastoreCallCount > 0 { // Note that "database" is used for the keys here instead of // "datastore" for historical reasons. w.intField("databaseCallCount", int64(e.datastoreCallCount)) w.floatField("databaseDuration", e.datastoreDuration.Seconds()) } if e.CrossProcess.IsSynthetics() { w.stringField("nr.syntheticsResourceId", e.CrossProcess.Synthetics.ResourceID) w.stringField("nr.syntheticsJobId", e.CrossProcess.Synthetics.JobID) w.stringField("nr.syntheticsMonitorId", e.CrossProcess.Synthetics.MonitorID) if e.CrossProcess.SyntheticsInfo != nil { w.stringField("nr.syntheticsType", e.CrossProcess.SyntheticsInfo.Type) w.stringField("nr.syntheticsInitiator", e.CrossProcess.SyntheticsInfo.Initiator) for attrName, attrValue := range e.CrossProcess.SyntheticsInfo.Attributes { if attrName != "" { w.stringField(fmt.Sprintf("nr.synthetics%s%s", strings.ToUpper(attrName[0:1]), attrName[1:]), attrValue) } } } } } // sharedBetterCATIntrinsics reports intrinsics that are shared // by Transaction, TransactionError, and Slow SQL func sharedBetterCATIntrinsics(e *txnEvent, w *jsonFieldsWriter) { if e.BetterCAT.Enabled { if p := e.BetterCAT.Inbound; nil != p { if p.HasNewRelicTraceInfo { w.stringField("parent.type", p.Type) w.stringField("parent.app", p.App) w.stringField("parent.account", p.Account) w.floatField("parent.transportDuration", p.TransportDuration.Seconds()) } w.stringField("parent.transportType", e.BetterCAT.TransportType) } w.stringField("guid", e.BetterCAT.TxnID) w.stringField("traceId", e.BetterCAT.TraceID) w.writerField("priority", e.BetterCAT.Priority) w.boolField("sampled", e.BetterCAT.Sampled) } } // MarshalJSON is used for testing. func (e *txnEvent) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, 256)) e.WriteJSON(buf) return buf.Bytes(), nil } type txnEvents struct { *analyticsEvents } func newTxnEvents(max int) *txnEvents { return &txnEvents{ analyticsEvents: newAnalyticsEvents(max), } } func (events *txnEvents) AddTxnEvent(e *txnEvent, priority priority) { // Synthetics events always get priority: normal event priorities are in the // range [0.0,1.99999], so adding 2 means that a Synthetics event will always // win. if e.CrossProcess.IsSynthetics() { priority += 2.0 } events.addEvent(analyticsEvent{priority: priority, jsonWriter: e}) } func (events *txnEvents) MergeIntoHarvest(h *harvest) { h.TxnEvents.mergeFailed(events.analyticsEvents) } func (events *txnEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { return events.CollectorJSON(agentRunID) } func (events *txnEvents) EndpointMethod() string { return cmdTxnEvents } func (events *txnEvents) payloads(limit int) []payloadCreator { if events.NumSaved() < float64(limit) { return []payloadCreator{events} } e1, e2 := events.split() return []payloadCreator{ &txnEvents{analyticsEvents: e1}, &txnEvents{analyticsEvents: e2}, } } go-agent-3.42.0/v3/newrelic/txn_events_test.go000066400000000000000000000203221510742411500212230ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "encoding/json" "testing" "time" "github.com/newrelic/go-agent/v3/internal/cat" ) func testTxnEventJSON(t testing.TB, e *txnEvent, expect string) { // Type assertion to support early Go versions. if h, ok := t.(interface { Helper() }); ok { h.Helper() } js, err := json.Marshal(e) if nil != err { t.Error(err) return } expect = compactJSONString(expect) if string(js) != expect { t.Errorf("\nexpect=%s\nactual=%s\n", expect, string(js)) } } var ( sampleTxnEvent = txnEvent{ FinalName: "myName", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, Start: timeFromUnixMilliseconds(1488393111000), Duration: 2 * time.Second, TotalTime: 3 * time.Second, Zone: apdexNone, Attrs: nil, } sampleTxnEventWithOldCAT = txnEvent{ FinalName: "myOldName", BetterCAT: betterCAT{ Enabled: false, }, Start: timeFromUnixMilliseconds(1488393111000), Duration: 2 * time.Second, TotalTime: 3 * time.Second, Zone: apdexNone, Attrs: nil, } sampleTxnEventWithError = txnEvent{ FinalName: "myName", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, Start: timeFromUnixMilliseconds(1488393111000), Duration: 2 * time.Second, TotalTime: 3 * time.Second, Zone: apdexNone, Attrs: nil, HasError: true, } ) func TestTxnEventMarshal(t *testing.T) { e := sampleTxnEvent testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "error":false, "duration":2, "totalTime":3, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, {}, {}]`) } func TestTxnEventMarshalWithApdex(t *testing.T) { e := sampleTxnEvent e.Zone = apdexFailing testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "nr.apdexPerfZone":"F", "error":false, "duration":2, "totalTime":3, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, {}, {}]`) } func TestTxnEventMarshalWithDatastoreExternal(t *testing.T) { e := sampleTxnEvent e.externalCallCount = 22 e.externalDuration = 1122334 * time.Millisecond e.datastoreCallCount = 33 e.datastoreDuration = 5566778 * time.Millisecond testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "error":false, "duration":2, "externalCallCount":22, "externalDuration":1122.334, "databaseCallCount":33, "databaseDuration":5566.778, "totalTime":3, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, {}, {}]`) } func TestTxnEventMarshalWithInboundCaller(t *testing.T) { e := sampleTxnEvent e.BetterCAT.Inbound = &payload{ Type: "Browser", App: "caller-app", Account: "caller-account", ID: "caller-id", TransactionID: "caller-parent-id", TracedID: "trip-id", TransportDuration: 2 * time.Second, HasNewRelicTraceInfo: true, } e.BetterCAT.TraceID = "trip-id" e.BetterCAT.TransportType = "HTTP" testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "error":false, "duration":2, "totalTime":3, "parent.type": "Browser", "parent.app": "caller-app", "parent.account": "caller-account", "parent.transportDuration": 2, "parent.transportType": "HTTP", "guid":"txn-id", "traceId":"trip-id", "priority":0.500000, "sampled":false, "parentId": "caller-parent-id", "parentSpanId": "caller-id" }, {}, {}]`) } func TestTxnEventMarshalWithInboundCallerOldCAT(t *testing.T) { e := sampleTxnEventWithOldCAT e.BetterCAT.Inbound = &payload{ Type: "Browser", App: "caller-app", Account: "caller-account", ID: "caller-id", TransactionID: "caller-parent-id", TracedID: "trip-id", TransportDuration: 2 * time.Second, } testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myOldName", "timestamp":1488393111000, "error":false, "duration":2, "totalTime":3 }, {}, {}]`) } func TestTxnEventMarshalWithAttributes(t *testing.T) { aci := config{Config: defaultConfig()} aci.TransactionEvents.Attributes.Exclude = append(aci.TransactionEvents.Attributes.Exclude, "zap") aci.TransactionEvents.Attributes.Exclude = append(aci.TransactionEvents.Attributes.Exclude, AttributeHostDisplayName) cfg := createAttributeConfig(aci, true) attr := newAttributes(cfg) attr.Agent.Add(AttributeHostDisplayName, "exclude me", nil) attr.Agent.Add(AttributeRequestMethod, "GET", nil) addUserAttribute(attr, "zap", 123, destAll) addUserAttribute(attr, "zip", 456, destAll) e := sampleTxnEvent e.Attrs = attr testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "error":false, "duration":2, "totalTime":3, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, { "zip":456 }, { "request.method":"GET" }]`) } func TestTxnEventsPayloadsEmpty(t *testing.T) { events := newTxnEvents(10) ps := events.payloads(5) if len(ps) != 1 { t.Error(ps) } if data, err := ps[0].Data("agentRunID", time.Now()); data != nil || err != nil { t.Error(data, err) } } func TestTxnEventsPayloadsUnderLimit(t *testing.T) { events := newTxnEvents(10) for i := 0; i < 4; i++ { events.AddTxnEvent(&txnEvent{}, priority(float32(i)/10.0)) } ps := events.payloads(5) if len(ps) != 1 { t.Error(ps) } if data, err := ps[0].Data("agentRunID", time.Now()); data == nil || err != nil { t.Error(data, err) } } func TestTxnEventsPayloadsOverLimit(t *testing.T) { events := newTxnEvents(10) for i := 0; i < 6; i++ { events.AddTxnEvent(&txnEvent{}, priority(float32(i)/10.0)) } ps := events.payloads(5) if len(ps) != 2 { t.Error(ps) } if data, err := ps[0].Data("agentRunID", time.Now()); data == nil || err != nil { t.Error(data, err) } if data, err := ps[1].Data("agentRunID", time.Now()); data == nil || err != nil { t.Error(data, err) } } func TestTxnEventsSynthetics(t *testing.T) { events := newTxnEvents(1) regular := &txnEvent{ FinalName: "Regular", Start: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Duration: 2 * time.Second, Zone: apdexNone, Attrs: nil, } synthetics := &txnEvent{ FinalName: "Synthetics", Start: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Duration: 2 * time.Second, Zone: apdexNone, Attrs: nil, CrossProcess: txnCrossProcess{ Type: txnCrossProcessSynthetics, Synthetics: &cat.SyntheticsHeader{ ResourceID: "resource", JobID: "job", MonitorID: "monitor", }, }, } events.AddTxnEvent(regular, 1.99999) // Check that the event was saved. if saved := events.analyticsEvents.events[0].jsonWriter; saved != regular { t.Errorf("unexpected saved event: expected=%v; got=%v", regular, saved) } // The priority sampling algorithm is implemented using isLowerPriority(). In // the case of an event pool with a single event, an incoming event with the // same priority would kick out the event already in the pool. To really test // whether synthetics are given highest deference, add a synthetics event // with a really low priority and affirm it kicks out the event already in the // pool. events.AddTxnEvent(synthetics, 0.0) // Check that the event was saved and its priority was appropriately augmented. if saved := events.analyticsEvents.events[0].jsonWriter; saved != synthetics { t.Errorf("unexpected saved event: expected=%v; got=%v", synthetics, saved) } if priority := events.analyticsEvents.events[0].priority; priority != 2.0 { t.Errorf("synthetics event has unexpected priority: %f", priority) } } func TestTxnEventMarshalWithError(t *testing.T) { e := sampleTxnEventWithError testTxnEventJSON(t, &e, `[ { "type":"Transaction", "name":"myName", "timestamp":1488393111000, "error":true, "duration":2, "totalTime":3, "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false }, {}, {}]`) } go-agent-3.42.0/v3/newrelic/txn_trace.go000066400000000000000000000277241510742411500177730ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "container/heap" "sort" "time" "github.com/newrelic/go-agent/v3/internal/jsonx" ) // See https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Trace-LEGACY.md type traceNodeHeap []traceNode type traceNodeParams struct { attributes map[string]jsonWriter StackTrace stackTrace TransactionGUID string exclusiveDurationMillis *float64 } type traceNode struct { start segmentTime stop segmentTime threadID uint64 duration time.Duration traceNodeParams name string } func (h traceNodeHeap) Len() int { return len(h) } func (h traceNodeHeap) Less(i, j int) bool { return h[i].duration < h[j].duration } func (h traceNodeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } // Push and Pop are unused: only heap.Init and heap.Fix are used. func (h traceNodeHeap) Push(x interface{}) {} func (h traceNodeHeap) Pop() interface{} { return nil } // txnTrace contains the work in progress transaction trace. type txnTrace struct { Enabled bool SegmentThreshold time.Duration StackTraceThreshold time.Duration nodes traceNodeHeap maxNodes int } // getMaxNodes allows the maximum number of nodes to be overwritten for unit // tests. func (trace *txnTrace) getMaxNodes() int { if 0 != trace.maxNodes { return trace.maxNodes } return maxTxnTraceNodes } // considerNode exists to prevent unnecessary calls to witnessNode: constructing // the metric name and params map requires allocations. func (trace *txnTrace) considerNode(end segmentEnd) bool { return trace.Enabled && (end.duration >= trace.SegmentThreshold) } func (trace *txnTrace) witnessNode(end segmentEnd, name string, attrs spanAttributeMap, externalGUID string) { node := traceNode{ start: end.start, stop: end.stop, duration: end.duration, threadID: end.threadID, name: name, } node.attributes = attrs node.TransactionGUID = externalGUID if !trace.considerNode(end) { return } if trace.nodes == nil { trace.nodes = make(traceNodeHeap, 0, startingTxnTraceNodes) } if end.exclusive >= trace.StackTraceThreshold { node.StackTrace = getStackTrace() } if max := trace.getMaxNodes(); len(trace.nodes) < max { trace.nodes = append(trace.nodes, node) if len(trace.nodes) == max { heap.Init(trace.nodes) } return } if node.duration <= trace.nodes[0].duration { return } trace.nodes[0] = node heap.Fix(trace.nodes, 0) } // harvestTrace contains a finished transaction trace ready for serialization to // the collector. type harvestTrace struct { txnEvent Trace txnTrace } type nodeDetails struct { name string relativeStart time.Duration relativeStop time.Duration traceNodeParams } func printNodeStart(buf *bytes.Buffer, n nodeDetails) { // time.Seconds() is intentionally not used here. Millisecond // precision is enough. relativeStartMillis := n.relativeStart.Nanoseconds() / (1000 * 1000) relativeStopMillis := n.relativeStop.Nanoseconds() / (1000 * 1000) buf.WriteByte('[') jsonx.AppendInt(buf, relativeStartMillis) buf.WriteByte(',') jsonx.AppendInt(buf, relativeStopMillis) buf.WriteByte(',') jsonx.AppendString(buf, n.name) buf.WriteByte(',') w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') if nil != n.StackTrace { w.writerField("backtrace", n.StackTrace) } if nil != n.exclusiveDurationMillis { w.floatField("exclusive_duration_millis", *n.exclusiveDurationMillis) } if "" != n.TransactionGUID { w.stringField("transaction_guid", n.TransactionGUID) } for k, v := range n.attributes { w.writerField(k, v) } buf.WriteByte('}') buf.WriteByte(',') buf.WriteByte('[') } func printChildren(buf *bytes.Buffer, traceStart time.Time, nodes sortedTraceNodes, next int, stop *segmentStamp, threadID uint64) int { firstChild := true for { if next >= len(nodes) { // No more children to print. break } if nodes[next].threadID != threadID { // The next node is not of the same thread. Due to the // node sorting, all nodes of the same thread should be // together. break } if stop != nil && nodes[next].start.Stamp >= *stop { // Make sure this node is a child of the parent that is // being printed. break } if firstChild { firstChild = false } else { buf.WriteByte(',') } printNodeStart(buf, nodeDetails{ name: nodes[next].name, relativeStart: nodes[next].start.Time.Sub(traceStart), relativeStop: nodes[next].stop.Time.Sub(traceStart), traceNodeParams: nodes[next].traceNodeParams, }) next = printChildren(buf, traceStart, nodes, next+1, &nodes[next].stop.Stamp, threadID) buf.WriteString("]]") } return next } type sortedTraceNodes []*traceNode func (s sortedTraceNodes) Len() int { return len(s) } func (s sortedTraceNodes) Less(i, j int) bool { // threadID is the first sort key and start.Stamp is the second key. if s[i].threadID == s[j].threadID { return s[i].start.Stamp < s[j].start.Stamp } return s[i].threadID < s[j].threadID } func (s sortedTraceNodes) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // MarshalJSON is used for testing. // // TODO: Eliminate this entirely by using harvestTraces.Data(). func (trace *harvestTrace) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, 100+100*trace.Trace.nodes.Len())) trace.writeJSON(buf) return buf.Bytes(), nil } func (trace *harvestTrace) writeJSON(buf *bytes.Buffer) { nodes := make(sortedTraceNodes, len(trace.Trace.nodes)) for i := 0; i < len(nodes); i++ { nodes[i] = &trace.Trace.nodes[i] } sort.Sort(nodes) buf.WriteByte('[') // begin trace jsonx.AppendInt(buf, trace.Start.UnixNano()/1000) buf.WriteByte(',') jsonx.AppendFloat(buf, trace.Duration.Seconds()*1000.0) buf.WriteByte(',') jsonx.AppendString(buf, trace.FinalName) buf.WriteByte(',') if uri, _ := trace.Attrs.GetAgentValue(AttributeRequestURI, destTxnTrace); "" != uri { jsonx.AppendString(buf, uri) } else { buf.WriteString("null") } buf.WriteByte(',') buf.WriteByte('[') // begin trace data // If the trace string pool is used, insert another array here. jsonx.AppendFloat(buf, 0.0) // unused timestamp buf.WriteByte(',') // buf.WriteString("{}") // unused: formerly request parameters buf.WriteByte(',') // buf.WriteString("{}") // unused: formerly custom parameters buf.WriteByte(',') // printNodeStart(buf, nodeDetails{ // begin outer root name: "ROOT", relativeStart: 0, relativeStop: trace.Duration, }) // exclusive_duration_millis field is added to fix the transaction trace // summary tab. If exclusive_duration_millis is not provided, the UIs // will calculate exclusive time, which doesn't work for this root node // since all async goroutines are children of this root. exclusiveDurationMillis := trace.Duration.Seconds() * 1000.0 details := nodeDetails{ // begin inner root name: trace.FinalName, relativeStart: 0, relativeStop: trace.Duration, } details.exclusiveDurationMillis = &exclusiveDurationMillis printNodeStart(buf, details) for next := 0; next < len(nodes); { if next > 0 { buf.WriteByte(',') } // We put each thread's nodes into the root node instead of the // node that spawned the thread. This approach is simple and // works when the segment which spawned a thread has been pruned // from the trace. Each call to printChildren prints one // thread. next = printChildren(buf, trace.Start, nodes, next, nil, nodes[next].threadID) } buf.WriteString("]]") // end outer root buf.WriteString("]]") // end inner root buf.WriteByte(',') buf.WriteByte('{') buf.WriteString(`"agentAttributes":`) agentAttributesJSON(trace.Attrs, buf, destTxnTrace) buf.WriteByte(',') buf.WriteString(`"userAttributes":`) userAttributesJSON(trace.Attrs, buf, destTxnTrace, nil) buf.WriteByte(',') buf.WriteString(`"intrinsics":`) intrinsicsJSON(&trace.txnEvent, buf, false) buf.WriteByte('}') // If the trace string pool is used, end another array here. buf.WriteByte(']') // end trace data // catGUID buf.WriteByte(',') if trace.CrossProcess.Used() && trace.CrossProcess.GUID != "" { jsonx.AppendString(buf, trace.CrossProcess.GUID) } else if trace.BetterCAT.Enabled { jsonx.AppendString(buf, trace.BetterCAT.TraceID) } else if !trace.BetterCAT.Enabled && trace.CrossProcess.GUID != "" { jsonx.AppendString(buf, trace.txnEvent.TxnID) } else { buf.WriteString(`""`) } buf.WriteByte(',') // buf.WriteString(`null`) // reserved for future use buf.WriteByte(',') // buf.WriteString(`false`) // ForcePersist is not yet supported buf.WriteByte(',') // buf.WriteString(`null`) // X-Ray sessions not supported buf.WriteByte(',') // // Synthetics are supported: if trace.CrossProcess.IsSynthetics() { jsonx.AppendString(buf, trace.CrossProcess.Synthetics.ResourceID) } else { buf.WriteString(`""`) } buf.WriteByte(']') // end trace } type txnTraceHeap []*harvestTrace func (h *txnTraceHeap) isEmpty() bool { return 0 == len(*h) } func newTxnTraceHeap(max int) *txnTraceHeap { h := make(txnTraceHeap, 0, max) heap.Init(&h) return &h } // Implement sort.Interface. func (h txnTraceHeap) Len() int { return len(h) } func (h txnTraceHeap) Less(i, j int) bool { return h[i].Duration < h[j].Duration } func (h txnTraceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } // Implement heap.Interface. func (h *txnTraceHeap) Push(x interface{}) { *h = append(*h, x.(*harvestTrace)) } func (h *txnTraceHeap) Pop() interface{} { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func (h *txnTraceHeap) isKeeper(t *harvestTrace) bool { if len(*h) < cap(*h) { return true } return t.Duration >= (*h)[0].Duration } func (h *txnTraceHeap) addTxnTrace(t *harvestTrace) { if len(*h) < cap(*h) { heap.Push(h, t) return } if t.Duration <= (*h)[0].Duration { return } heap.Pop(h) heap.Push(h, t) } type harvestTraces struct { regular *txnTraceHeap synthetics *txnTraceHeap } func newHarvestTraces() *harvestTraces { return &harvestTraces{ regular: newTxnTraceHeap(maxRegularTraces), synthetics: newTxnTraceHeap(maxSyntheticsTraces), } } func (traces *harvestTraces) Len() int { return traces.regular.Len() + traces.synthetics.Len() } func (traces *harvestTraces) Witness(trace harvestTrace) { traceHeap := traces.regular if trace.CrossProcess.IsSynthetics() { traceHeap = traces.synthetics } if traceHeap.isKeeper(&trace) { cpy := new(harvestTrace) *cpy = trace traceHeap.addTxnTrace(cpy) } } func (traces *harvestTraces) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { if traces.Len() == 0 { return nil, nil } // This estimate is used to guess the size of the buffer. No worries if // the estimate is small since the buffer will be lengthened as // necessary. This is just about minimizing reallocations. estimate := 512 for _, t := range *traces.regular { estimate += 100 * t.Trace.nodes.Len() } for _, t := range *traces.synthetics { estimate += 100 * t.Trace.nodes.Len() } buf := bytes.NewBuffer(make([]byte, 0, estimate)) buf.WriteByte('[') jsonx.AppendString(buf, agentRunID) buf.WriteByte(',') buf.WriteByte('[') // use a function to add traces to the buffer to avoid duplicating comma // logic in both loops firstTrace := true addTrace := func(trace *harvestTrace) { if firstTrace { firstTrace = false } else { buf.WriteByte(',') } trace.writeJSON(buf) } for _, trace := range *traces.regular { addTrace(trace) } for _, trace := range *traces.synthetics { addTrace(trace) } buf.WriteByte(']') buf.WriteByte(']') return buf.Bytes(), nil } func (traces *harvestTraces) slice() []*harvestTrace { out := make([]*harvestTrace, 0, traces.Len()) out = append(out, (*traces.regular)...) out = append(out, (*traces.synthetics)...) return out } func (traces *harvestTraces) MergeIntoHarvest(h *harvest) {} func (traces *harvestTraces) EndpointMethod() string { return cmdTxnTraces } go-agent-3.42.0/v3/newrelic/txn_trace_test.go000066400000000000000000001145251510742411500210260ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "strconv" "testing" "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/cat" "github.com/newrelic/go-agent/v3/internal/logger" ) func float64Ptr(f float64) *float64 { return &f } func TestTxnTrace(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 t1 := startSegment(txndata, thread, start.Add(1*time.Second)) t2 := startSegment(txndata, thread, start.Add(2*time.Second)) qParams, err := vetQueryParameters(map[string]interface{}{"zip": 1}) if nil != err { t.Error("error creating query params", err) } endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t2, Now: start.Add(3 * time.Second), Product: "MySQL", Operation: "SELECT", Collection: "my_table", ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", QueryParameters: qParams, Database: "my_db", Host: "db-server-1", PortPathOrID: "3306", }) t3 := startSegment(txndata, thread, start.Add(4*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t3, Now: start.Add(5 * time.Second), URL: parseURL("http://example.com/zip/zap?secret=shhh"), Logger: logger.ShimLogger{}, }) endBasicSegment(txndata, thread, t1, start.Add(6*time.Second), "t1") t4 := startSegment(txndata, thread, start.Add(7*time.Second)) t5 := startSegment(txndata, thread, start.Add(8*time.Second)) t6 := startSegment(txndata, thread, start.Add(9*time.Second)) endBasicSegment(txndata, thread, t6, start.Add(10*time.Second), "t6") endBasicSegment(txndata, thread, t5, start.Add(11*time.Second), "t5") t7 := startSegment(txndata, thread, start.Add(12*time.Second)) endDatastoreSegment(endDatastoreParams{ TxnData: txndata, Thread: thread, Start: t7, Now: start.Add(13 * time.Second), Product: "MySQL", Operation: "SELECT", // no collection }) t8 := startSegment(txndata, thread, start.Add(14*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t8, Now: start.Add(15 * time.Second), URL: nil, Logger: logger.ShimLogger{}, }) endBasicSegment(txndata, thread, t4, start.Add(16*time.Second), "t4") t9 := startSegment(txndata, thread, start.Add(17*time.Second)) endMessageSegment(endMessageParams{ TxnData: txndata, Thread: thread, Start: t9, Now: start.Add(18 * time.Second), Logger: nil, DestinationName: "MyTopic", Library: "Kafka", DestinationType: "Topic", }) acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) addUserAttribute(attr, "zap", 123, destAll) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{"zap": 123}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, "totalTime": 30, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/t1", RelativeStartMillis: 1000, RelativeStopMillis: 6000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{ { SegmentName: "Datastore/statement/MySQL/my_table/SELECT", RelativeStartMillis: 2000, RelativeStopMillis: 3000, Attributes: map[string]interface{}{ "db.instance": "my_db", "peer.hostname": "db-server-1", "peer.address": "db-server-1:3306", "db.statement": "INSERT INTO users (name, age) VALUES ($1, $2)", "query_parameters": "map[zip:1]", }, Children: []internal.WantTraceSegment{}, }, { SegmentName: "External/example.com/http", RelativeStartMillis: 4000, RelativeStopMillis: 5000, Attributes: map[string]interface{}{ "http.url": "http://example.com/zip/zap", }, Children: []internal.WantTraceSegment{}, }, }, }, { SegmentName: "Custom/t4", RelativeStartMillis: 7000, RelativeStopMillis: 16000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/t5", RelativeStartMillis: 8000, RelativeStopMillis: 11000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/t6", RelativeStartMillis: 9000, RelativeStopMillis: 10000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }, { SegmentName: "Datastore/operation/MySQL/SELECT", RelativeStartMillis: 12000, RelativeStopMillis: 13000, Attributes: map[string]interface{}{ "db.statement": "'SELECT' on 'unknown' using 'MySQL'", }, Children: []internal.WantTraceSegment{}, }, { SegmentName: "External/unknown/http", RelativeStartMillis: 14000, RelativeStopMillis: 15000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }, { SegmentName: "MessageBroker/Kafka/Topic/Produce/Named/MyTopic", RelativeStartMillis: 17000, RelativeStopMillis: 18000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }}, }, }}) } func TestTxnTraceNoNodes(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: nil, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, "totalTime": 30, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{}, }}, }, }}) } func TestTxnTraceAsync(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{ TraceIDGenerator: internal.NewTraceIDGenerator(12345), } thread1 := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 txndata.BetterCAT.Sampled = true txndata.ShouldCollectSpanEvents = trueFunc txndata.ShouldCreateSpanGUID = trueFunc t1s1 := startSegment(txndata, thread1, start.Add(1*time.Second)) t1s2 := startSegment(txndata, thread1, start.Add(2*time.Second)) thread2 := newTracingThread(txndata) t2s1 := startSegment(txndata, thread2, start.Add(3*time.Second)) endBasicSegment(txndata, thread1, t1s2, start.Add(4*time.Second), "thread1.segment2") endBasicSegment(txndata, thread2, t2s1, start.Add(5*time.Second), "thread2.segment1") thread3 := newTracingThread(txndata) t3s1 := startSegment(txndata, thread3, start.Add(6*time.Second)) t3s2 := startSegment(txndata, thread3, start.Add(7*time.Second)) endBasicSegment(txndata, thread1, t1s1, start.Add(8*time.Second), "thread1.segment1") endBasicSegment(txndata, thread3, t3s2, start.Add(9*time.Second), "thread3.segment2") endBasicSegment(txndata, thread3, t3s1, start.Add(10*time.Second), "thread3.segment1") if tt := thread1.TotalTime(); tt != 7*time.Second { t.Error(tt) } if tt := thread2.TotalTime(); tt != 2*time.Second { t.Error(tt) } if tt := thread3.TotalTime(); tt != 4*time.Second { t.Error(tt) } if len(txndata.SpanEvents) != 5 { t.Fatal("Expected 5 span events, but found: ", txndata.SpanEvents) } for _, e := range txndata.SpanEvents { if e.GUID == "" || e.ParentID == "" { t.Error(e.GUID, e.ParentID) } } spanEventT1S2 := txndata.SpanEvents[0] spanEventT2S1 := txndata.SpanEvents[1] spanEventT1S1 := txndata.SpanEvents[2] spanEventT3S2 := txndata.SpanEvents[3] spanEventT3S1 := txndata.SpanEvents[4] if txndata.rootSpanID == "" { t.Error(txndata.rootSpanID) } if spanEventT1S1.ParentID != txndata.rootSpanID { t.Error(spanEventT1S1.ParentID, txndata.rootSpanID) } if spanEventT1S2.ParentID != spanEventT1S1.GUID { t.Error(spanEventT1S2.ParentID, spanEventT1S1.GUID) } if spanEventT2S1.ParentID != txndata.rootSpanID { t.Error(spanEventT2S1.ParentID, txndata.rootSpanID) } if spanEventT3S1.ParentID != txndata.rootSpanID { t.Error(spanEventT3S1.ParentID, txndata.rootSpanID) } if spanEventT3S2.ParentID != spanEventT3S1.GUID { t.Error(spanEventT3S2.ParentID, spanEventT3S1.GUID) } ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: nil, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "totalTime": 30, "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/thread1.segment1", RelativeStartMillis: 1000, RelativeStopMillis: 8000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/thread1.segment2", RelativeStartMillis: 2000, RelativeStopMillis: 4000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }, { SegmentName: "Custom/thread2.segment1", RelativeStartMillis: 3000, RelativeStopMillis: 5000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/thread3.segment1", RelativeStartMillis: 6000, RelativeStopMillis: 10000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/thread3.segment2", RelativeStartMillis: 7000, RelativeStopMillis: 9000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }, }, }}, }, }}) } func TestTxnTraceOldCAT(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 txndata.CrossProcess.Init(true, false, replyAccountOne) txndata.CrossProcess.GUID = "0123456789" appData, err := txndata.CrossProcess.CreateAppData("WebTransaction/Go/otherService", 2*time.Second, 3*time.Second, 123) if nil != err { t.Fatal(err) } resp := &http.Response{ Header: appDataToHTTPHeader(appData), } t3 := startSegment(txndata, thread, start.Add(4*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t3, Now: start.Add(5 * time.Second), URL: parseURL("http://example.com/zip/zap?secret=shhh"), Response: resp, Logger: logger.ShimLogger{}, }) acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) addUserAttribute(attr, "zap", 123, destAll) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{"zap": 123}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{"totalTime": 30}, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{ { SegmentName: "ExternalTransaction/example.com/1#1/WebTransaction/Go/otherService", RelativeStartMillis: 4000, RelativeStopMillis: 5000, Attributes: map[string]interface{}{ "http.url": "http://example.com/zip/zap", "transaction_guid": "0123456789", }, Children: []internal.WantTraceSegment{}, }, }, }}, }, }}) } func TestTxnTraceExcludeURI(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) tr := &txnData{} tr.TxnTrace.Enabled = true tr.TxnTrace.StackTraceThreshold = 1 * time.Hour tr.TxnTrace.SegmentThreshold = 0 c := config{Config: defaultConfig()} c.TransactionTracer.Attributes.Exclude = []string{"request.uri"} acfg := createAttributeConfig(c, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: tr.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "totalTime": 0, "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{}, }}, }, }}) } func TestTxnTraceNoSegmentsNoAttributes(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 20 * time.Second, TotalTime: 30 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(20 * 1000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{ "totalTime": 30, "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 20000, Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, Children: []internal.WantTraceSegment{}, }}, }, }}) } func TestTxnTraceSlowestNodesSaved(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 0 txndata.TxnTrace.maxNodes = 5 durations := []int{5, 4, 6, 3, 7, 2, 8, 1, 9} now := start for _, d := range durations { s := startSegment(txndata, thread, now) now = now.Add(time.Duration(d) * time.Second) endBasicSegment(txndata, thread, s, now, strconv.Itoa(d)) } acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 123 * time.Second, TotalTime: 200 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(123000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 200, "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 123000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 123000, Attributes: map[string]interface{}{"exclusive_duration_millis": 123000}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/5", RelativeStartMillis: 0, RelativeStopMillis: 5000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/6", RelativeStartMillis: 9000, RelativeStopMillis: 15000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/7", RelativeStartMillis: 18000, RelativeStopMillis: 25000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/8", RelativeStartMillis: 27000, RelativeStopMillis: 35000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/9", RelativeStartMillis: 36000, RelativeStopMillis: 45000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }}, }, }}) } func TestTxnTraceSegmentThreshold(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour txndata.TxnTrace.SegmentThreshold = 7 * time.Second txndata.TxnTrace.maxNodes = 5 durations := []int{5, 4, 6, 3, 7, 2, 8, 1, 9} now := start for _, d := range durations { s := startSegment(txndata, thread, now) now = now.Add(time.Duration(d) * time.Second) endBasicSegment(txndata, thread, s, now, strconv.Itoa(d)) } acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) ht := newHarvestTraces() ht.regular.addTxnTrace(&harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 123 * time.Second, TotalTime: 200 * time.Second, FinalName: "WebTransaction/Go/hello", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", TraceID: "trace-id", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(123000.0), MetricName: "WebTransaction/Go/hello", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 200, "guid": "txn-id", "traceId": "trace-id", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 123000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/hello", RelativeStartMillis: 0, RelativeStopMillis: 123000, Attributes: map[string]interface{}{"exclusive_duration_millis": 123000}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/7", RelativeStartMillis: 18000, RelativeStopMillis: 25000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/8", RelativeStartMillis: 27000, RelativeStopMillis: 35000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/9", RelativeStartMillis: 36000, RelativeStopMillis: 45000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, }, }}, }, }}) } func TestEmptyHarvestTraces(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) ht := newHarvestTraces() js, err := ht.Data("12345", start) if nil != err || nil != js { t.Error(string(js), err) } } func TestLongestTraceSaved(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/3", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id-3", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 5 * time.Second, TotalTime: 6 * time.Second, FinalName: "WebTransaction/Go/5", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id-5", TraceID: "trace-id-5", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 4 * time.Second, TotalTime: 7 * time.Second, FinalName: "WebTransaction/Go/4", Attrs: attr, BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id-4", TraceID: "trace-id-4", Priority: 0.5, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{{ DurationMillis: float64Ptr(5000.0), MetricName: "WebTransaction/Go/5", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 6, "guid": "txn-id-5", "traceId": "trace-id-5", "priority": 0.500000, "sampled": false, }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 5000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/5", RelativeStartMillis: 0, RelativeStopMillis: 5000, Attributes: map[string]interface{}{"exclusive_duration_millis": 5000}, Children: []internal.WantTraceSegment{}, }}, }, }}) } func TestTxnTraceStackTraceThreshold(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} thread := &tracingThread{} txndata.TxnTrace.Enabled = true txndata.TxnTrace.StackTraceThreshold = 2 * time.Second txndata.TxnTrace.SegmentThreshold = 0 txndata.TxnTrace.maxNodes = 5 // below stack trace threshold t1 := startSegment(txndata, thread, start.Add(1*time.Second)) endBasicSegment(txndata, thread, t1, start.Add(2*time.Second), "t1") // not above stack trace threshold w/out params t2 := startSegment(txndata, thread, start.Add(2*time.Second)) endBasicSegment(txndata, thread, t2, start.Add(4*time.Second), "t2") // node above stack trace threshold w/ params t3 := startSegment(txndata, thread, start.Add(4*time.Second)) endExternalSegment(endExternalParams{ TxnData: txndata, Thread: thread, Start: t3, Now: start.Add(6 * time.Second), URL: parseURL("http://example.com/zip/zap?secret=shhh"), Logger: logger.ShimLogger{}, }) ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/3", }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{ { DurationMillis: float64Ptr(3000.0), MetricName: "WebTransaction/Go/3", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{}, Intrinsics: map[string]interface{}{"totalTime": 4}, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 3000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/3", RelativeStartMillis: 0, RelativeStopMillis: 3000, Attributes: map[string]interface{}{"exclusive_duration_millis": 3000}, Children: []internal.WantTraceSegment{ { SegmentName: "Custom/t1", RelativeStartMillis: 1000, RelativeStopMillis: 2000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "Custom/t2", RelativeStartMillis: 2000, RelativeStopMillis: 4000, Attributes: map[string]interface{}{"backtrace": internal.MatchAnything}, Children: []internal.WantTraceSegment{}, }, { SegmentName: "External/example.com/http", RelativeStartMillis: 4000, RelativeStopMillis: 6000, Attributes: map[string]interface{}{ "backtrace": internal.MatchAnything, "http.url": "http://example.com/zip/zap", }, Children: []internal.WantTraceSegment{}, }, }, }}, }, }, }) } func TestTxnTraceSynthetics(t *testing.T) { start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true acfg := createAttributeConfig(config{Config: defaultConfig()}, true) attr := newAttributes(acfg) attr.Agent.Add(AttributeRequestURI, "/url", nil) ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/3", Attrs: attr, CrossProcess: txnCrossProcess{ Type: txnCrossProcessSynthetics, Synthetics: &cat.SyntheticsHeader{ ResourceID: "resource", }, }, }, Trace: txndata.TxnTrace, }) ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 5 * time.Second, TotalTime: 6 * time.Second, FinalName: "WebTransaction/Go/5", Attrs: attr, CrossProcess: txnCrossProcess{ Type: txnCrossProcessSynthetics, Synthetics: &cat.SyntheticsHeader{ ResourceID: "resource", }, }, }, Trace: txndata.TxnTrace, }) ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 4 * time.Second, TotalTime: 5 * time.Second, FinalName: "WebTransaction/Go/4", Attrs: attr, CrossProcess: txnCrossProcess{ Type: txnCrossProcessSynthetics, Synthetics: &cat.SyntheticsHeader{ ResourceID: "resource", }, }, }, Trace: txndata.TxnTrace, }) expectTxnTraces(t, ht, []internal.WantTxnTrace{ { DurationMillis: float64Ptr(3000.0), MetricName: "WebTransaction/Go/3", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 4, "synthetics_resource_id": "resource", }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 3000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/3", RelativeStartMillis: 0, RelativeStopMillis: 3000, Attributes: map[string]interface{}{"exclusive_duration_millis": 3000}, Children: []internal.WantTraceSegment{}, }}, }, }, { DurationMillis: float64Ptr(5000.0), MetricName: "WebTransaction/Go/5", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 6, "synthetics_resource_id": "resource", }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 5000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/5", RelativeStartMillis: 0, RelativeStopMillis: 5000, Attributes: map[string]interface{}{"exclusive_duration_millis": 5000}, Children: []internal.WantTraceSegment{}, }}, }, }, { DurationMillis: float64Ptr(4000.0), MetricName: "WebTransaction/Go/4", UserAttributes: map[string]interface{}{}, AgentAttributes: map[string]interface{}{"request.uri": "/url"}, Intrinsics: map[string]interface{}{ "totalTime": 5, "synthetics_resource_id": "resource", }, Root: internal.WantTraceSegment{ SegmentName: "ROOT", RelativeStartMillis: 0, RelativeStopMillis: 4000, Attributes: map[string]interface{}{}, Children: []internal.WantTraceSegment{{ SegmentName: "WebTransaction/Go/4", RelativeStartMillis: 0, RelativeStopMillis: 4000, Attributes: map[string]interface{}{"exclusive_duration_millis": 4000}, Children: []internal.WantTraceSegment{}, }}, }, }, }) } func TestTraceJSON(t *testing.T) { // Have one test compare exact JSON to ensure that all misc fields (such // as the trailing `null,false,null,""`) are what we expect. start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/trace", Attrs: nil, }, Trace: txndata.TxnTrace, }) expect := `[ "12345", [ [ 1417136460000000, 3000, "WebTransaction/Go/trace", null, [0,{},{}, [ 0, 3000, "ROOT", {}, [[0,3000,"WebTransaction/Go/trace",{"exclusive_duration_millis":3000},[]]] ], { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{"totalTime":4} } ],"",null,false,null,"" ] ] ]` js, err := ht.Data("12345", start) if nil != err { t.Fatal(err) } testExpectedJSON(t, expect, string(js)) } func TestTraceCatGUID(t *testing.T) { // Test catGUID is properly set in outbound json when CAT is enabled start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/trace", Attrs: nil, CrossProcess: txnCrossProcess{ Type: 1, GUID: "this is guid", }, }, Trace: txndata.TxnTrace, }) expect := `[ "12345", [ [ 1417136460000000, 3000, "WebTransaction/Go/trace", null, [0,{},{}, [ 0, 3000, "ROOT", {}, [[0,3000,"WebTransaction/Go/trace",{"exclusive_duration_millis":3000},[]]] ], { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{"totalTime":4} } ],"this is guid",null,false,null,"" ] ] ]` js, err := ht.Data("12345", start) if nil != err { t.Fatal(err) } testExpectedJSON(t, expect, string(js)) } func TestTraceDistributedTracingGUID(t *testing.T) { // Test catGUID is properly set in outbound json when DT is enabled start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) txndata := &txnData{} txndata.TxnTrace.Enabled = true ht := newHarvestTraces() ht.Witness(harvestTrace{ txnEvent: txnEvent{ Start: start, Duration: 3 * time.Second, TotalTime: 4 * time.Second, FinalName: "WebTransaction/Go/trace", Attrs: nil, BetterCAT: betterCAT{ Enabled: true, TxnID: "this is guid", TraceID: "trace-id", }, }, Trace: txndata.TxnTrace, }) expect := `[ "12345", [ [ 1417136460000000, 3000, "WebTransaction/Go/trace", null, [0,{},{}, [ 0, 3000, "ROOT", {}, [[0,3000,"WebTransaction/Go/trace",{"exclusive_duration_millis":3000},[]]] ], { "agentAttributes":{}, "userAttributes":{}, "intrinsics":{ "totalTime":4, "guid":"this is guid", "traceId":"trace-id", "priority":0.000000, "sampled":false } } ],"trace-id",null,false,null,"" ] ] ]` js, err := ht.Data("12345", start) if nil != err { t.Fatal(err) } testExpectedJSON(t, expect, string(js)) } func BenchmarkWitnessNode(b *testing.B) { trace := &txnTrace{ Enabled: true, SegmentThreshold: 0, // save all segments StackTraceThreshold: 1 * time.Hour, // no stack traces maxNodes: 100 * 1000, } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { end := segmentEnd{ duration: time.Duration(randUint32()) * time.Millisecond, exclusive: 0, } trace.witnessNode(end, "myNode", nil, "") } } go-agent-3.42.0/v3/newrelic/url.go000066400000000000000000000016021510742411500165710ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import "net/url" // safeURL removes sensitive information from a URL. func safeURL(u *url.URL) string { if nil == u { return "" } if "" != u.Opaque { // If the URL is opaque, we cannot be sure if it contains // sensitive information. return "" } // Omit user, query, and fragment information for security. ur := url.URL{ Scheme: u.Scheme, Host: u.Host, Path: u.Path, } return ur.String() } // safeURLFromString removes sensitive information from a URL. func safeURLFromString(rawurl string) string { u, err := url.Parse(rawurl) if nil != err { return "" } return safeURL(u) } // hostFromURL returns the URL's host. func hostFromURL(u *url.URL) string { if nil == u { return "" } if "" != u.Opaque { return "opaque" } return u.Host } go-agent-3.42.0/v3/newrelic/url_test.go000066400000000000000000000033751510742411500176410ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/url" "strings" "testing" "github.com/newrelic/go-agent/v3/internal/crossagent" ) func TestSafeURLNil(t *testing.T) { if out := safeURL(nil); "" != out { t.Error(out) } } func TestSafeURL(t *testing.T) { var testcases []struct { Testname string `json:"testname"` Expect string `json:"expected"` Input string `json:"input"` } err := crossagent.ReadJSON("url_clean.json", &testcases) if err != nil { t.Fatal(err) } for _, tc := range testcases { if strings.Contains(tc.Input, ";") { // This test case was over defensive: // http://www.ietf.org/rfc/rfc3986.txt continue } // Only use testcases which have a scheme, otherwise the urls // may not be valid and may not be correctly handled by // url.Parse. if strings.HasPrefix(tc.Input, "p:") { u, err := url.Parse(tc.Input) if nil != err { t.Error(tc.Testname, tc.Input, err) continue } out := safeURL(u) if out != tc.Expect { t.Error(tc.Testname, tc.Input, tc.Expect) } } } } func TestSafeURLFromString(t *testing.T) { out := safeURLFromString(`http://localhost:8000/hello?zip=zap`) if `http://localhost:8000/hello` != out { t.Error(out) } out = safeURLFromString("?????") if "" != out { t.Error(out) } } func TestHostFromURL(t *testing.T) { u, err := url.Parse("http://example.com/zip/zap?secret=shh") if nil != err { t.Fatal(err) } host := hostFromURL(u) if host != "example.com" { t.Error(host) } host = hostFromURL(nil) if host != "" { t.Error(host) } u, err = url.Parse("scheme:opaque") if nil != err { t.Fatal(err) } host = hostFromURL(u) if host != "opaque" { t.Error(host) } } go-agent-3.42.0/v3/newrelic/utilities.go000066400000000000000000000052571510742411500200140ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "bytes" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" ) // jsonString assists in logging JSON: Based on the formatter used to log // Context contents, the contents could be marshalled as JSON or just printed // directly. type jsonString string // MarshalJSON returns the jsonString unmodified without any escaping. func (js jsonString) MarshalJSON() ([]byte, error) { if "" == js { return []byte("null"), nil } return []byte(js), nil } func removeFirstSegment(name string) string { idx := strings.Index(name, "/") if -1 == idx { return name } return name[idx+1:] } func timeToIntMillis(t time.Time) int64 { return t.UnixNano() / (1000 * 1000) } func timeToFloatMilliseconds(t time.Time) float64 { return float64(t.UnixNano()) / float64(1000*1000) } // compactJSONString removes the whitespace from a JSON string. This function // will panic if the string provided is not valid JSON. Thus is must only be // used in testing code! func compactJSONString(js string) string { buf := new(bytes.Buffer) if err := json.Compact(buf, []byte(js)); err != nil { panic(fmt.Errorf("unable to compact JSON: %v", err)) } return buf.String() } // getContentLengthFromHeader gets the content length from a HTTP header, or -1 // if no content length is available. func getContentLengthFromHeader(h http.Header) int64 { if cl := h.Get("Content-Length"); cl != "" { if contentLength, err := strconv.ParseInt(cl, 10, 64); err == nil { return contentLength } } return -1 } // stringLengthByteLimit truncates strings using a byte-limit boundary and // avoids terminating in the middle of a multibyte character. func stringLengthByteLimit(str string, byteLimit int) string { if len(str) <= byteLimit { return str } limitIndex := 0 for pos := range str { if pos > byteLimit { break } limitIndex = pos } return str[0:limitIndex] } func timeFromUnixMilliseconds(millis uint64) time.Time { secs := int64(millis / 1000) msecsRemaining := int64(millis % 1000) nsecsRemaining := msecsRemaining * (1000 * 1000) return time.Unix(secs, nsecsRemaining) } // timeToUnixMilliseconds converts a time into a Unix timestamp in millisecond // units. func timeToUnixMilliseconds(tm time.Time) uint64 { return uint64(tm.UnixNano()) / uint64(1000*1000) } // minorVersion takes a given version string and returns only the major and // minor portions of it. If the input is malformed, it returns the input // untouched. func minorVersion(v string) string { split := strings.SplitN(v, ".", 3) if len(split) < 2 { return v } return split[0] + "." + split[1] } go-agent-3.42.0/v3/newrelic/utilities_test.go000066400000000000000000000063551510742411500210530ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "net/http" "testing" "time" ) func TestRemoveFirstSegment(t *testing.T) { testcases := []struct { input string expected string }{ {input: "no_seperators", expected: "no_seperators"}, {input: "heyo/zip/zap", expected: "zip/zap"}, {input: "ends_in_slash/", expected: ""}, {input: "☃☃☃/✓✓✓/heyo", expected: "✓✓✓/heyo"}, {input: "☃☃☃/", expected: ""}, {input: "/", expected: ""}, {input: "", expected: ""}, } for _, tc := range testcases { out := removeFirstSegment(tc.input) if out != tc.expected { t.Fatal(tc.input, out, tc.expected) } } } func TestTimeToFloatMilliseconds(t *testing.T) { tm := time.Unix(123, 456789000) if ms := timeToFloatMilliseconds(tm); ms != 123456.789 { t.Error(ms) } } func TestCompactJSON(t *testing.T) { in := ` { "zip": 1}` out := compactJSONString(in) if out != `{"zip":1}` { t.Fatal(in, out) } } func TestGetContentLengthFromHeader(t *testing.T) { // Nil header. if cl := getContentLengthFromHeader(nil); cl != -1 { t.Errorf("unexpected content length: expected -1; got %d", cl) } // Empty header. header := make(http.Header) if cl := getContentLengthFromHeader(header); cl != -1 { t.Errorf("unexpected content length: expected -1; got %d", cl) } // Invalid header. header.Set("Content-Length", "foo") if cl := getContentLengthFromHeader(header); cl != -1 { t.Errorf("unexpected content length: expected -1; got %d", cl) } // Zero header. header.Set("Content-Length", "0") if cl := getContentLengthFromHeader(header); cl != 0 { t.Errorf("unexpected content length: expected 0; got %d", cl) } // Valid, non-zero header. header.Set("Content-Length", "1024") if cl := getContentLengthFromHeader(header); cl != 1024 { t.Errorf("unexpected content length: expected 1024; got %d", cl) } } func TestStringLengthByteLimit(t *testing.T) { testcases := []struct { input string limit int expect string }{ {"", 255, ""}, {"awesome", -1, ""}, {"awesome", 0, ""}, {"awesome", 1, "a"}, {"awesome", 7, "awesome"}, {"awesome", 20, "awesome"}, {"日本\x80語", 10, "日本\x80語"}, // bad unicode {"日本", 1, ""}, {"日本", 2, ""}, {"日本", 3, "日"}, {"日本", 4, "日"}, {"日本", 5, "日"}, {"日本", 6, "日本"}, {"日本", 7, "日本"}, } for _, tc := range testcases { out := stringLengthByteLimit(tc.input, tc.limit) if out != tc.expect { t.Error(tc.input, tc.limit, tc.expect, out) } } } func TestTimeToAndFromUnixMilliseconds(t *testing.T) { t1 := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) millis := timeToUnixMilliseconds(t1) if millis != 1417136460000 { t.Fatal(millis) } t2 := timeFromUnixMilliseconds(millis) if t1.UnixNano() != t2.UnixNano() { t.Fatal(t1, t2) } } func TestMinorVersion(t *testing.T) { testcases := []struct { input string expect string }{ {"go1.13", "go1.13"}, {"go1.13.1", "go1.13"}, {"go1.13.1.0", "go1.13"}, {"purple", "purple"}, } for _, test := range testcases { if actual := minorVersion(test.input); actual != test.expect { t.Errorf("incorrect result: expect=%s actual=%s", test.expect, actual) } } } go-agent-3.42.0/v3/newrelic/version.go000066400000000000000000000010141510742411500174510ustar00rootroot00000000000000// Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package newrelic import ( "runtime" "github.com/newrelic/go-agent/v3/internal" ) const ( // Version is the full string version of this Go Agent. Version = "3.42.0" ) var ( goVersionSimple = minorVersion(runtime.Version()) ) func init() { internal.TrackUsage("Go", "Version", Version) internal.TrackUsage("Go", "Runtime", "Version", goVersionSimple) internal.TrackUsage("Go", "gRPC", "Version", grpcVersion) }