diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 000000000..577a2b50a --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,27 @@ +# Set to true to add reviewers to pull requests +addReviewers: true + +# Set to true to add assignees to pull requests +addAssignees: false + +# 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 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..888ffd041 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 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" diff --git a/.github/workflows/autoassign.yml b/.github/workflows/autoassign.yml new file mode 100644 index 000000000..2960b1772 --- /dev/null +++ b/.github/workflows/autoassign.yml @@ -0,0 +1,10 @@ +name: 'Auto Assign' +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + add-reviews: + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@v1.2.0 \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ba1254f0..a25740c8b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,320 +1,143 @@ -# Copyright 2020 New Relic Corporation. All rights reserved. +# Copyright 2023 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 - name: Go Agent CI - on: pull_request +env: + # Specifies which go version to run integration tests on + INTEGRATION_TESTS_GO_VERSION: 1.21.5 jobs: - go-agent: - runs-on: ubuntu-18.04 - env: - # Required when using older versions of Go that do not support gomod. - GOPATH: ${{ github.workspace }} - + go-agent-v3: + runs-on: ubuntu-latest strategy: # if one test fails, do not abort the rest fail-fast: false matrix: include: + # Core Tests on 3 most recent major Go versions + - go-version: 1.19.0 + dirs: v3/newrelic,v3/internal,v3/examples + - go-version: 1.20.0 + dirs: v3/newrelic,v3/internal,v3/examples + - go-version: 1.21.0 + dirs: v3/newrelic,v3/internal,v3/examples - # v2 agent - # 1.3.x and 1.4.x are failing with a linker error, skip those for now - # - go-version: 1.3.x - # dirs: . - # - go-version: 1.4.x - # dirs: . - - go-version: 1.5.x - dirs: . - - go-version: 1.6.x - dirs: . - - go-version: 1.7.x - dirs: . - - go-version: 1.8.x - dirs: . - - go-version: 1.9.x - dirs: . - - go-version: 1.10.x - dirs: . - - go-version: 1.11.x - dirs: . - - go-version: 1.12.x - dirs: . - - go-version: 1.13.x - dirs: . - - # v2 integrations - - go-version: 1.13.x - # only versions up to 0.24.0 of awssdkv2 are supported by this code - pin: github.com/aws/aws-sdk-go-v2@v0.24.0 - dirs: _integrations/nrawssdk - - go-version: 1.13.x - dirs: _integrations/nrecho - pin: github.com/labstack/echo@v3.3.10 - - go-version: 1.13.x - dirs: _integrations/nrgorilla/v1 - - go-version: 1.13.x - dirs: _integrations/nrlogrus - - go-version: 1.13.x - dirs: _integrations/nrlogxi/v1 - - go-version: 1.13.x - dirs: _integrations/nrpkgerrors - - go-version: 1.13.x - dirs: _integrations/nrlambda - - go-version: 1.13.x - dirs: _integrations/nrmysql - - go-version: 1.13.x - dirs: _integrations/nrpq - - go-version: 1.13.x - dirs: _integrations/nrsqlite3 - - go-version: 1.13.x - dirs: _integrations/nrgrpc - # As of October 2019, errors result from go get -u github.com/micro/go-micro - # As of June 2020, confirmed errors still result - # - go-version: 1.13.x - # dirs: _integrations/nrmicro - # As of Jul 2022, we have depreciated the legacy nrnats,nrmssql, nrzap, and nrstan integrations tests. - # These tests still exist under the v3 versions of the integrations. - - go-version: 1.13.x - dirs: _integrations/logcontext - - go-version: 1.13.x - dirs: _integrations/nrhttprouter - - go-version: 1.13.x - dirs: _integrations/nrb3 - - go-version: 1.13.x - dirs: _integrations/nrmongo + # Integration Tests on highest Supported Go Version + - dirs: v3/integrations/nramqp + - dirs: v3/integrations/nrfasthttp + - dirs: v3/integrations/nrsarama + - dirs: v3/integrations/logcontext/nrlogrusplugin + - dirs: v3/integrations/logcontext-v2/nrlogrus + - dirs: v3/integrations/logcontext-v2/nrzerolog + - dirs: v3/integrations/logcontext-v2/nrzap + - dirs: v3/integrations/logcontext-v2/nrslog + - dirs: v3/integrations/logcontext-v2/nrwriter + - dirs: v3/integrations/logcontext-v2/zerologWriter + - dirs: v3/integrations/logcontext-v2/logWriter + - dirs: v3/integrations/nrawssdk-v1 + - dirs: v3/integrations/nrawssdk-v2 + - dirs: v3/integrations/nrecho-v3 + - dirs: v3/integrations/nrecho-v4 + - dirs: v3/integrations/nrelasticsearch-v7 + - dirs: v3/integrations/nrgin + - dirs: v3/integrations/nrgorilla + - dirs: v3/integrations/nrgraphgophers + - dirs: v3/integrations/nrlogrus + - dirs: v3/integrations/nrlogxi + - dirs: v3/integrations/nrpkgerrors + - dirs: v3/integrations/nrlambda + - dirs: v3/integrations/nrmysql + - dirs: v3/integrations/nrpq + - dirs: v3/integrations/nrpgx5 + - dirs: v3/integrations/nrpq/example/sqlx + - dirs: v3/integrations/nrredis-v7 + - dirs: v3/integrations/nrredis-v9 + - dirs: v3/integrations/nrsqlite3 + - dirs: v3/integrations/nrsnowflake + - dirs: v3/integrations/nrgrpc + - dirs: v3/integrations/nrmicro + - dirs: v3/integrations/nrnats + - dirs: v3/integrations/nrstan + - dirs: v3/integrations/nrstan/test + - dirs: v3/integrations/nrstan/examples + - dirs: v3/integrations/logcontext + - dirs: v3/integrations/nrzap + - dirs: v3/integrations/nrhttprouter + - dirs: v3/integrations/nrb3 + - dirs: v3/integrations/nrmongo + - dirs: v3/integrations/nrgraphqlgo,v3/integrations/nrgraphqlgo/example + - dirs: v3/integrations/nrmssql + - dirs: v3/integrations/nropenai + - dirs: v3/integrations/nrslog steps: - - name: Install Go - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go-version }} - - name: Checkout Code - uses: actions/checkout@v1 + uses: actions/checkout@v2 with: - # Required when using older versions of Go that do not support gomod. - # Note the required presence of the /go-agent/ directory at the - # beginning of this path. It is required in order to match the - # ${{ github.workspace }} used by the GOPATH env var. pwd when cloning - # the repo is /go-agent/ whereas ${{ github.workspace }} - # returns /go-agent/ whereas ${{ github.workspace }} - # returns /go-agent/ whereas ${{ github.workspace }} - # returns 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](/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](/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](/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 @@ -862,7 +1255,7 @@ 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](http://support.newrelic.com/) or our + 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. @@ -896,7 +1289,7 @@ include: ### Bug Fixes * Fixed an issue in the - [`nrhttprouter`](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) + [`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 @@ -942,7 +1335,7 @@ package. // (see https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing) txn := currentTxn() - req, err := http.NewRequest("GET", "http://example.com", nil) + req, err := http.NewRequest("GET", "https://example.com", nil) if nil != err { log.Fatalln(err) } @@ -1118,9 +1511,9 @@ It can be configured as follows: ### New Features * Added support for [HttpRouter](https://github.com/julienschmidt/httprouter) in - the new [_integrations/nrhttprouter](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) package. This package allows you to easily instrument inbound requests through the HttpRouter framework. + 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](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) + * [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 @@ -1203,7 +1596,7 @@ package. This package supports instrumentation for servers, clients, publishers * 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("http://example.com") + u, _ := url.Parse("https://example.com") webReq := newrelic.NewStaticWebRequest(hdrs, u, "GET", newrelic.TransportHTTP) txn := app.StartTransaction("My-Transaction", nil, nil) txn.SetWebRequest(webReq) @@ -1435,8 +1828,8 @@ package. This package supports instrumentation for servers and clients. 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](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v1) - * [v2 Documentation](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v2) + * [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 @@ -1535,7 +1928,7 @@ txn.Application().RecordCustomEvent("customerOrder", map[string]interface{}{ * Added support for [Echo](https://echo.labstack.com) in the new `nrecho` package. - * [Documentation](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrecho) + * [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 @@ -1598,7 +1991,7 @@ transactions. Example use: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(nil, client.Transport) -request, _ := http.NewRequest("GET", "http://example.com", nil) +request, _ := http.NewRequest("GET", "https://example.com", nil) request = newrelic.RequestWithTransactionContext(request, txn) resp, err := client.Do(request) ``` @@ -1761,7 +2154,7 @@ txn.NoticeError(newrelic.Error{ * Added support for [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) in the new `nrgin` package. - * [Documentation](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1) + * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1) * [Example](examples/_gin/main.go) ## 1.8.0 @@ -1774,9 +2167,9 @@ txn.NoticeError(newrelic.Error{ ## 1.7.0 -* Added support for [gorilla/mux](http://github.com/gorilla/mux) in the new `nrgorilla` +* Added support for [gorilla/mux](https://github.com/gorilla/mux) in the new `nrgorilla` package. - * [Documentation](http://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1) + * [Documentation](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1) * [Example](examples/_gorilla/main.go) ## 1.6.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 766c8b2de..6e767d86c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,4 +37,4 @@ For more information about CLAs, please check out Alex Russell’s excellent pos ## 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, see https://newrelicusers-signup.herokuapp.com/. +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). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..566e372ad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# This file is used to build the docker image for the Go Agent's GitHub Action tests +# Default go version if no arguments passed in +ARG GO_VERSION=1.19 + +# Takes in go version +FROM golang:${GO_VERSION} as builder + +# Set working directory and run go mod tidy +WORKDIR /app +# Copy source code files +COPY . . diff --git a/GUIDE.md b/GUIDE.md index ce300b5f0..a6df00b9a 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -127,7 +127,7 @@ app, err := newrelic.NewApplication( 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, 0644) +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"), @@ -401,7 +401,7 @@ ways to use this functionality: ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) - request, _ := http.NewRequest("GET", "http://example.com", nil) + 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) diff --git a/MIGRATION.md b/MIGRATION.md index 1b74019d1..b3368c123 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -455,7 +455,7 @@ var _ newrelic.ErrorAttributer = MyErrorType{} ```go client := &http.Client{} client.Transport = newrelic.NewRoundTripper(txn, client.Transport) - req, _ := http.NewRequest("GET", "http://example.com", nil) + req, _ := http.NewRequest("GET", "https://example.com", nil) client.Do(req) ``` diff --git a/README.md b/README.md index 97c559116..8fe6b0921 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![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) +# 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 @@ -13,7 +13,7 @@ Go is a compiled language, and doesn’t use a virtual machine. This means that ### Compatibility and Requirements -For the latest version of the agent, Go 1.17+ is required. +For the latest version of the agent, Go 1.18+ is required. Linux, OS X, and Windows (Vista, Server 2008 and later) are supported. @@ -97,18 +97,28 @@ package primitives can be found [here](GUIDE.md#datastore-segments). | [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 | -#### Agent Logging +#### AI | 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 | -| [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 | +| [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 diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index bf21381e1..000000000 --- a/ROADMAP.md +++ /dev/null @@ -1,26 +0,0 @@ -# Go Agent Roadmap - -## Product Vision -The goal of the Go agent is to provide complete visibility into the health of your service. The agent provides metrics about the runtime health of your service and the process it runs in, and traces that show how specific requests are performing. It also provides information about the environment in which it is running, so you can identify issues with specific hosts, regions, deployments, and other facets. - -New Relic is moving toward OpenTelemetry. OpenTelemetry is a unified standard for service instrumentation. You can use our [OpenTelemetry Exporter](https://github.com/newrelic/opentelemetry-exporter-go) today, and will soon see a shim will convert New Relic Go Agent data to OpenTelemetry. OpenTelemetry will include a broad set of high-quality community-contributed instrumentation and a powerful vendor-neutral API for adding your own instrumentation. - - -## Roadmap -**The Go instrumentation roadmap project is found [here](https://github.com/orgs/newrelic/projects/24)**. - -This roadmap project is broken down into the following sections: - -- **Done**: - - This section contains features that were recently completed. -- **Now**: - - This section contains features that are currently in progress. -- **Next**: - - This section contains work planned within the next three months. These features may still be de-prioritized and moved to Future. -- **Future**: - - This section is for ideas for future work that is aligned with the product vision and possible opportunities for community contribution. It contains a list of features that anyone can implement. No guarantees can be provided on if or when these features will be completed. - - - -### Disclaimers -This roadmap is subject to change at any time. Future items should not be considered commitments. diff --git a/_integrations/README.md b/_integrations/README.md deleted file mode 100644 index 2ec0fb926..000000000 --- a/_integrations/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The integrations in this directory are for the now deprecated v2 New Relic Go -Agent. Integrations for the most recent v3 version of the agent can be found -in [v3/integrations](../v3/integrations). diff --git a/_integrations/logcontext/README.md b/_integrations/logcontext/README.md deleted file mode 100644 index ba5c24dcb..000000000 --- a/_integrations/logcontext/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# _integrations/logcontext [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/logcontext?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_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/_integrations/logcontext). diff --git a/_integrations/logcontext/logcontext.go b/_integrations/logcontext/logcontext.go deleted file mode 100644 index 45032fa52..000000000 --- a/_integrations/logcontext/logcontext.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package logcontext - -import newrelic "github.com/newrelic/go-agent" - -// 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) -} diff --git a/_integrations/logcontext/nrlogrusplugin/README.md b/_integrations/logcontext/nrlogrusplugin/README.md deleted file mode 100644 index e0f36542f..000000000 --- a/_integrations/logcontext/nrlogrusplugin/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/logcontext/nrlogrusplugin [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin) - -Package `nrlogrusplugin` decorates logs for sending to the New Relic backend. - -```go -import "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin). diff --git a/_integrations/logcontext/nrlogrusplugin/example/main.go b/_integrations/logcontext/nrlogrusplugin/example/main.go deleted file mode 100644 index 9ac6e8a1d..000000000 --- a/_integrations/logcontext/nrlogrusplugin/example/main.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "fmt" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin" - "github.com/sirupsen/logrus" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func doFunction2(txn newrelic.Transaction, e *logrus.Entry) { - defer newrelic.StartSegment(txn, "doFunction2").End() - e.Error("In doFunction2") -} - -func doFunction1(txn newrelic.Transaction, e *logrus.Entry) { - defer newrelic.StartSegment(txn, "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") - - cfg := newrelic.NewConfig("Logrus Log Decoration", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - - app, err := newrelic.NewApplication(cfg) - 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", nil, nil) - - // 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() -} diff --git a/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin.go b/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin.go deleted file mode 100644 index 8702eaf8e..000000000 --- a/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin.go +++ /dev/null @@ -1,189 +0,0 @@ -// 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 want to enable the New Relic logging product and see -// your log messages in the New Relic UI. -// -// 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/_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: -// -// cfg := NewConfig("Example Application", "__YOUR_NEW_RELIC_LICENSE_KEY__") -// cfg.DistributedTracer.Enabled = 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" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/logcontext" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/jsonx" - "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)) - } -} diff --git a/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin_test.go b/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin_test.go deleted file mode 100644 index cdaa777fc..000000000 --- a/_integrations/logcontext/nrlogrusplugin/nrlogrusplugin_test.go +++ /dev/null @@ -1,397 +0,0 @@ -// 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" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" - "github.com/newrelic/go-agent/internal/sysinfo" - "github.com/sirupsen/logrus" -) - -var ( - testTime = time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - matchAnything = struct{}{} -) - -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", nil, nil) - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestLogNoContext", - "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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestLogNoTxn", - "timestamp": float64(1417136460000), - }) -} - -func TestLogDistributedTracingDisabled(t *testing.T) { - app := integrationsupport.NewTestApp(nil, nil) - txn := app.StartTransaction("TestLogDistributedTracingDisabled", nil, nil) - out := bytes.NewBuffer([]byte{}) - log := newTestLogger(out) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestLogDistributedTracingDisabled", - "timestamp": float64(1417136460000), - }) -} - -func TestLogSampledFalse(t *testing.T) { - app := integrationsupport.NewTestApp( - func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleNothing{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - }, - func(cfg *newrelic.Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - }) - txn := app.StartTransaction("TestLogSampledFalse", nil, nil) - out := bytes.NewBuffer([]byte{}) - log := newTestLogger(out) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestLogSampledFalse", - "timestamp": float64(1417136460000), - "trace.id": "d9466896a525ccbf", - }) -} - -func TestLogSampledTrue(t *testing.T) { - app := integrationsupport.NewTestApp( - func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - }, - func(cfg *newrelic.Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - }) - txn := app.StartTransaction("TestLogSampledTrue", nil, nil) - out := bytes.NewBuffer([]byte{}) - log := newTestLogger(out) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestLogSampledTrue", - "span.id": "bcfb32e050b264b8", - "timestamp": float64(1417136460000), - "trace.id": "d9466896a525ccbf", - }) -} - -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.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - }, - func(cfg *newrelic.Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - }) - txn := app.StartTransaction("TestEntryUsedTwice1", nil, nil) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestEntryUsedTwice", - "span.id": "bcfb32e050b264b8", - "timestamp": float64(1417136460000), - "trace.id": "d9466896a525ccbf", - }) - - // 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", nil, nil) - ctx = newrelic.NewContext(context.Background(), txn) - host, _ = sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestEntryUsedTwice", - "timestamp": float64(1417136460000), - }) -} - -func TestEntryError(t *testing.T) { - app := integrationsupport.NewTestApp(nil, nil) - txn := app.StartTransaction("TestEntryError", nil, nil) - out := bytes.NewBuffer([]byte{}) - log := newTestLogger(out) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestEntryError", - "timestamp": float64(1417136460000), - }) -} - -func TestWithCustomField(t *testing.T) { - app := integrationsupport.NewTestApp(nil, nil) - txn := app.StartTransaction("TestWithCustomField", nil, nil) - out := bytes.NewBuffer([]byte{}) - log := newTestLogger(out) - ctx := newrelic.NewContext(context.Background(), txn) - host, _ := sysinfo.Hostname() - 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": "github.com/newrelic/go-agent/_integrations/logcontext/nrlogrusplugin.TestWithCustomField", - "timestamp": float64(1417136460000), - "zip": "zap", - }) -} - -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), - }) -} diff --git a/_integrations/nrawssdk/README.md b/_integrations/nrawssdk/README.md deleted file mode 100644 index 3886ce4f7..000000000 --- a/_integrations/nrawssdk/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# _integrations/nrawssdk [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk) - -Integrations for AWS SDKs versions 1 and 2. diff --git a/_integrations/nrawssdk/internal/internal.go b/_integrations/nrawssdk/internal/internal.go deleted file mode 100644 index 924206c63..000000000 --- a/_integrations/nrawssdk/internal/internal.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "context" - "net/http" - "reflect" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -type contextKeyType struct{} - -var segmentContextKey = contextKeyType(struct{}{}) - -type endable interface{ End() error } - -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 -} - -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" { - 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: newrelic.StartSegmentNow(txn), - } - } else { - segment = newrelic.StartExternalSegment(txn, input.HTTPRequest) - } - - 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, hdr http.Header) { - if segment, ok := ctx.Value(segmentContextKey).(endable); ok { - if id := getRequestID(hdr); "" != id { - txn := newrelic.FromContext(ctx) - integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSRequestID, id) - } - segment.End() - } -} diff --git a/_integrations/nrawssdk/internal/internal_test.go b/_integrations/nrawssdk/internal/internal_test.go deleted file mode 100644 index 4bcb9f907..000000000 --- a/_integrations/nrawssdk/internal/internal_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/http" - "strings" - "testing" - - requestv2 "github.com/aws/aws-sdk-go-v2/aws" - restv2 "github.com/aws/aws-sdk-go-v2/private/protocol/rest" - "github.com/aws/aws-sdk-go-v2/service/lambda" - requestv1 "github.com/aws/aws-sdk-go/aws/request" - restv1 "github.com/aws/aws-sdk-go/private/protocol/rest" -) - -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"}, - } - - for i, test := range testcases { - if out := getRequestID(test.hdr); test.expected != out { - t.Error(i, out, test.hdr, test.expected) - } - } - - // Make sure our assumptions still hold against aws-sdk-go - for _, test := range testcases { - req := &requestv1.Request{ - HTTPResponse: &http.Response{ - Header: test.hdr, - }, - } - restv1.UnmarshalMeta(req) - if out := getRequestID(test.hdr); req.RequestID != out { - t.Error("requestId assumptions incorrect", out, req.RequestID, - test.hdr, test.expected) - } - } - - // Make sure our assumptions still hold against aws-sdk-go-v2 - for _, test := range testcases { - req := &requestv2.Request{ - HTTPResponse: &http.Response{ - Header: test.hdr, - }, - Data: &lambda.InvokeOutput{}, - } - restv2.UnmarshalMeta(req) - if out := getRequestID(test.hdr); req.RequestID != out { - t.Error("requestId assumptions incorrect", out, req.RequestID, - test.hdr, test.expected) - } - } -} diff --git a/_integrations/nrawssdk/v1/README.md b/_integrations/nrawssdk/v1/README.md deleted file mode 100644 index c05194fb8..000000000 --- a/_integrations/nrawssdk/v1/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrawssdk/v1 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v1?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v1) - -Package `nrawssdk` instruments https://github.com/aws/aws-sdk-go requests. - -```go -import "github.com/newrelic/go-agent/_integrations/nrawssdk/v1" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v1). diff --git a/_integrations/nrawssdk/v1/nrawssdk.go b/_integrations/nrawssdk/v1/nrawssdk.go deleted file mode 100644 index b088c1cd5..000000000 --- a/_integrations/nrawssdk/v1/nrawssdk.go +++ /dev/null @@ -1,87 +0,0 @@ -// 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" - internal "github.com/newrelic/go-agent/_integrations/nrawssdk/internal" - agentinternal "github.com/newrelic/go-agent/internal" -) - -func init() { agentinternal.TrackUsage("integration", "library", "aws-sdk-go") } - -func startSegment(req *request.Request) { - input := internal.StartSegmentInputs{ - HTTPRequest: req.HTTPRequest, - ServiceName: req.ClientInfo.ServiceName, - Operation: req.Operation.Name, - Region: req.ClientInfo.SigningRegion, - Params: req.Params, - } - req.HTTPRequest = internal.StartSegment(input) -} - -func endSegment(req *request.Request) { - ctx := req.HTTPRequest.Context() - internal.EndSegment(ctx, req.HTTPResponse.Header) -} - -// 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, - }) -} diff --git a/_integrations/nrawssdk/v1/nrawssdk_test.go b/_integrations/nrawssdk/v1/nrawssdk_test.go deleted file mode 100644 index 48bb5976c..000000000 --- a/_integrations/nrawssdk/v1/nrawssdk_test.go +++ /dev/null @@ -1,589 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrawssdk - -import ( - "bytes" - "errors" - "io/ioutil" - "net/http" - "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/service/dynamodb" - "github.com/aws/aws-sdk-go/service/lambda" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn) -} - -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, - "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", - }, - } - 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", - }, - } - 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, nil, nil) - - 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{ - genericSpan, externalSpan}) -} - -func TestInstrumentRequestDatastore(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - 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{ - genericSpan, datastoreSpan}) -} - -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, nil, nil) - - 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{ - genericSpan, externalSpan}) -} - -func TestInstrumentSessionDatastore(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - 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{ - genericSpan, datastoreSpan}) -} - -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, nil, nil) - - 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, nil, nil) - - 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, nil, nil) - - 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{ - genericSpan, externalSpanNoRequestID, externalSpan}) -} - -func TestRequestSentTwice(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - 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{ - genericSpan, externalSpan, externalSpan}) -} - -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, nil, nil) - - 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{ - genericSpan, externalSpanNoRequestID}) -} diff --git a/_integrations/nrawssdk/v2/README.md b/_integrations/nrawssdk/v2/README.md deleted file mode 100644 index dc2ca26ad..000000000 --- a/_integrations/nrawssdk/v2/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrawssdk/v2 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v2?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v2) - -Package `nrawssdk` instruments https://github.com/aws/aws-sdk-go-v2 requests. - -```go -import "github.com/newrelic/go-agent/_integrations/nrawssdk/v2" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrawssdk/v2). diff --git a/_integrations/nrawssdk/v2/nrawssdk.go b/_integrations/nrawssdk/v2/nrawssdk.go deleted file mode 100644 index 4b2300a0d..000000000 --- a/_integrations/nrawssdk/v2/nrawssdk.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Package nrawssdk instruments https://github.com/aws/aws-sdk-go-v2 requests. -package nrawssdk - -import ( - "github.com/aws/aws-sdk-go-v2/aws" - internal "github.com/newrelic/go-agent/_integrations/nrawssdk/internal" - agentinternal "github.com/newrelic/go-agent/internal" -) - -func init() { agentinternal.TrackUsage("integration", "library", "aws-sdk-go-v2") } - -func startSegment(req *aws.Request) { - input := internal.StartSegmentInputs{ - HTTPRequest: req.HTTPRequest, - ServiceName: req.Metadata.ServiceName, - Operation: req.Operation.Name, - Region: req.Metadata.SigningRegion, - Params: req.Params, - } - req.HTTPRequest = internal.StartSegment(input) -} - -func endSegment(req *aws.Request) { - ctx := req.HTTPRequest.Context() - internal.EndSegment(ctx, req.HTTPResponse.Header) -} - -// InstrumentHandlers will add instrumentation to the given *aws.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 a Config and see segments created for each -// invocation that uses that Config, call InstrumentHandlers with the config's -// Handlers and add the current Transaction to the `http.Request`'s Context: -// -// cfg, _ := external.LoadDefaultAWSConfig() -// cfg.Region = "us-west-2" -// // Add instrumentation to handlers -// nrawssdk.InstrumentHandlers(&cfg.Handlers) -// lambdaClient = lambda.New(cfg) -// -// req := lambdaClient.InvokeRequest(&lambda.InvokeInput{ -// ClientContext: aws.String("MyApp"), -// FunctionName: aws.String("Function"), -// InvocationType: lambda.InvocationTypeEvent, -// LogType: lambda.LogTypeTail, -// Payload: []byte("{}"), -// } -// // Add txn to http.Request's context -// ctx := newrelic.NewContext(req.Context(), txn) -// resp, err := req.Send(ctx) -// -// To add instrumentation to a Request and see a segment created just for the -// individual request, call InstrumentHandlers with the `aws.Request`'s -// Handlers and add the current Transaction to the `http.Request`'s Context: -// -// req := lambdaClient.InvokeRequest(&lambda.InvokeInput{ -// ClientContext: aws.String("MyApp"), -// FunctionName: aws.String("Function"), -// InvocationType: lambda.InvocationTypeEvent, -// LogType: lambda.LogTypeTail, -// Payload: []byte("{}"), -// } -// // Add instrumentation to handlers -// nrawssdk.InstrumentHandlers(&req.Handlers) -// // Add txn to http.Request's context -// ctx := newrelic.NewContext(req.Context(), txn) -// resp, err := req.Send(ctx) -func InstrumentHandlers(handlers *aws.Handlers) { - handlers.Send.SetFrontNamed(aws.NamedHandler{ - Name: "StartNewRelicSegment", - Fn: startSegment, - }) - handlers.Send.SetBackNamed(aws.NamedHandler{ - Name: "EndNewRelicSegment", - Fn: endSegment, - }) -} diff --git a/_integrations/nrawssdk/v2/nrawssdk_test.go b/_integrations/nrawssdk/v2/nrawssdk_test.go deleted file mode 100644 index c01269412..000000000 --- a/_integrations/nrawssdk/v2/nrawssdk_test.go +++ /dev/null @@ -1,582 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrawssdk - -import ( - "bytes" - "context" - "errors" - "io/ioutil" - "net/http" - "testing" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/external" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/aws/aws-sdk-go-v2/service/lambda" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn) -} - -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 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(instrument bool) aws.Config { - cfg, _ := external.LoadDefaultAWSConfig() - cfg.Credentials = fakeCreds.(aws.CredentialsProvider) - cfg.Region = "us-west-2" - cfg.HTTPClient = &http.Client{ - Transport: &fakeTransport{}, - } - - if instrument { - InstrumentHandlers(&cfg.Handlers) - } - return cfg -} - -const ( - requestID = "testing request id" - txnName = "aws-txn" -) - -var ( - genericSpan = internal.WantEvent{ - Intrinsics: map[string]interface{}{ - "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", - }, - } - 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", - }, - } - 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(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}, - }...) - datastoreMetrics = append(txnMetrics, []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}, - }...) -) - -func TestInstrumentRequestExternal(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := lambda.New(newConfig(false)) - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - req := client.InvokeRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, externalMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - genericSpan, externalSpan}) -} - -func TestInstrumentRequestDatastore(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := dynamodb.New(newConfig(false)) - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } - - req := client.DescribeTableRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, datastoreMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - genericSpan, datastoreSpan}) -} - -func TestInstrumentRequestExternalNoTxn(t *testing.T) { - client := lambda.New(newConfig(false)) - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - - req := client.InvokeRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } -} - -func TestInstrumentRequestDatastoreNoTxn(t *testing.T) { - client := dynamodb.New(newConfig(false)) - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } - - req := client.DescribeTableRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } -} - -func TestInstrumentConfigExternal(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := lambda.New(newConfig(true)) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - - req := client.InvokeRequest(input) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, externalMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - genericSpan, externalSpan}) -} - -func TestInstrumentConfigDatastore(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := dynamodb.New(newConfig(true)) - - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } - - req := client.DescribeTableRequest(input) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, datastoreMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - genericSpan, datastoreSpan}) -} - -func TestInstrumentConfigExternalNoTxn(t *testing.T) { - client := lambda.New(newConfig(true)) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - - req := client.InvokeRequest(input) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } -} - -func TestInstrumentConfigDatastoreNoTxn(t *testing.T) { - client := dynamodb.New(newConfig(true)) - - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } - - req := client.DescribeTableRequest(input) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } -} - -func TestInstrumentConfigExternalTxnNotInCtx(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := lambda.New(newConfig(true)) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - - req := client.InvokeRequest(input) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, txnMetrics) -} - -func TestInstrumentConfigDatastoreTxnNotInCtx(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := dynamodb.New(newConfig(true)) - - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } - - req := client.DescribeTableRequest(input) - ctx := req.Context() - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, txnMetrics) -} - -func TestDoublyInstrumented(t *testing.T) { - hs := &aws.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, nil, nil) - - cfg := newConfig(false) - cfg.HTTPClient = &http.Client{ - Transport: &firstFailingTransport{failing: true}, - } - - client := lambda.New(cfg) - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - req := client.InvokeRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - 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{ - genericSpan, externalSpanNoRequestID, externalSpan}) -} - -func TestRequestSentTwice(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName, nil, nil) - - client := lambda.New(newConfig(false)) - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - req := client.InvokeRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := newrelic.NewContext(req.Context(), txn) - - _, firstErr := req.Send(ctx) - if nil != firstErr { - t.Error(firstErr) - } - - _, secondErr := req.Send(ctx) - if nil != secondErr { - 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{ - genericSpan, externalSpan, externalSpan}) -} - -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, nil, nil) - - cfg := newConfig(false) - cfg.HTTPClient = &http.Client{ - Transport: &noRequestIDTransport{}, - } - - client := lambda.New(cfg) - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: lambda.InvocationTypeEvent, - LogType: lambda.LogTypeTail, - Payload: []byte("{}"), - } - req := client.InvokeRequest(input) - InstrumentHandlers(&req.Handlers) - ctx := newrelic.NewContext(req.Context(), txn) - - _, err := req.Send(ctx) - if nil != err { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, externalMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - genericSpan, externalSpanNoRequestID}) -} diff --git a/_integrations/nrb3/README.md b/_integrations/nrb3/README.md deleted file mode 100644 index 92f200e3b..000000000 --- a/_integrations/nrb3/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrb3 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrb3?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrb3) - -Package `nrb3` supports adding B3 headers to outgoing requests. - -```go -import "github.com/newrelic/go-agent/_integrations/nrb3" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrb3). diff --git a/_integrations/nrb3/example_test.go b/_integrations/nrb3/example_test.go deleted file mode 100644 index 0ac2941d5..000000000 --- a/_integrations/nrb3/example_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrb3 - -import ( - "fmt" - "log" - "net/http" - "os" - - newrelic "github.com/newrelic/go-agent" - "github.com/openzipkin/zipkin-go" - reporterhttp "github.com/openzipkin/zipkin-go/reporter/http" -) - -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: 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) -} - -// 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. -func Example_zipkinReporter() { - // import ( - // reporterhttp "github.com/openzipkin/zipkin-go/reporter/http" - // ) - 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) -} diff --git a/_integrations/nrb3/nrb3.go b/_integrations/nrb3/nrb3.go deleted file mode 100644 index 1f4e41625..000000000 --- a/_integrations/nrb3/nrb3.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrb3 - -import ( - "net/http" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -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: newrelic.StartSegmentNow(txn), - Request: request, - } - defer segment.End() - - md := txn.GetTraceMetadata() - addHeader(request, "X-B3-TraceId", md.TraceID) - addHeader(request, "X-B3-SpanId", t.idGen.GenerateTraceID()) - addHeader(request, "X-B3-ParentSpanId", md.SpanID) - addHeader(request, "X-B3-Sampled", txnSampled(txn)) - } - - return t.original.RoundTrip(request) -} diff --git a/_integrations/nrb3/nrb3_doc.go b/_integrations/nrb3/nrb3_doc.go deleted file mode 100644 index 94fa41a10..000000000 --- a/_integrations/nrb3/nrb3_doc.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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. -package nrb3 diff --git a/_integrations/nrb3/nrb3_test.go b/_integrations/nrb3/nrb3_test.go deleted file mode 100644 index aa498f63e..000000000 --- a/_integrations/nrb3/nrb3_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrb3 - -import ( - "net/http" - "testing" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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", nil, nil) - - 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.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(123) - } - app := integrationsupport.NewTestApp(replyfn, integrationsupport.DTEnabledCfgFn) - txn := app.StartTransaction("test", nil, nil) - - 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 != "94d1331706b6a2b3" { - t.Error("unexpected value for X-B3-TraceId header:", hdr) - } - if hdr := sent.Header.Get("X-B3-SpanId"); hdr != "5a4f2d1b7f0cf06d" { - t.Error("unexpected value for X-B3-SpanId header:", hdr) - } - if hdr := sent.Header.Get("X-B3-ParentSpanId"); hdr != "3ffe00369da8a3b6" { - 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.AdaptiveSampler = internal.SampleNothing{} - } - app := integrationsupport.NewTestApp(replyfn, integrationsupport.DTEnabledCfgFn) - txn := app.StartTransaction("test", nil, nil) - - 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) - } -} diff --git a/_integrations/nrecho/README.md b/_integrations/nrecho/README.md deleted file mode 100644 index ee8cab3c7..000000000 --- a/_integrations/nrecho/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrecho [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrecho?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrecho) - -Package `nrecho` instruments https://github.com/labstack/echo applications. - -```go -import "github.com/newrelic/go-agent/_integrations/nrecho" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrecho). diff --git a/_integrations/nrecho/example/main.go b/_integrations/nrecho/example/main.go deleted file mode 100644 index eb11b726b..000000000 --- a/_integrations/nrecho/example/main.go +++ /dev/null @@ -1,61 +0,0 @@ -// 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/labstack/echo/middleware" - "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrecho" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func getUser(c echo.Context) error { - id := c.Param("id") - - if txn := nrecho.FromContext(c); nil != txn { - txn.AddAttribute("userId", id) - } - - return c.String(http.StatusOK, id) -} - -func main() { - cfg := newrelic.NewConfig("Echo App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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.Use(middleware.Gzip()) - g.GET("/:id", getUser) - - // Start server - e.Start(":8000") -} diff --git a/_integrations/nrecho/nrecho.go b/_integrations/nrecho/nrecho.go deleted file mode 100644 index 15171d425..000000000 --- a/_integrations/nrecho/nrecho.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Package nrecho instruments https://github.com/labstack/echo applications. -// -// 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/_integrations/nrecho/example/main.go -package nrecho - -import ( - "net/http" - "reflect" - - "github.com/labstack/echo" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -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 { - ptr := handlerPointer(c.Handler()) - if ptr == handlerPointer(echo.NotFoundHandler) { - return "NotFoundHandler" - } - if ptr == handlerPointer(echo.MethodNotAllowedHandler) { - return "MethodNotAllowedHandler" - } - return 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 - txn := app.StartTransaction(transactionName(c), rw, c.Request()) - defer txn.End() - - c.Response().Writer = txn - - // 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 { - - txn.SetWebResponse(nil) - c.Response().Writer = rw - - if httperr, ok := err.(*echo.HTTPError); ok { - txn.WriteHeader(httperr.Code) - } else { - txn.WriteHeader(http.StatusInternalServerError) - } - } - - return - } - } -} diff --git a/_integrations/nrecho/nrecho_test.go b/_integrations/nrecho/nrecho_test.go deleted file mode 100644 index 86d29be41..000000000 --- a/_integrations/nrecho/nrecho_test.go +++ /dev/null @@ -1,250 +0,0 @@ -// 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/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -func TestBasicRoute(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - 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: "hello", - IsWeb: true, - }) - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/hello", - "nr.apdexPerfZone": "S", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": "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.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - e.GET("/hello", func(c echo.Context) error { - txn := FromContext(c) - if nil != txn { - 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: "hello", - IsWeb: true, - NumErrors: 1, - }) -} - -func TestNotFoundHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - - 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, - }) -} - -func TestMethodNotAllowedHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - 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, - }) -} - -func TestReturnsHTTPError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - 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: "hello", - IsWeb: true, - NumErrors: 1, - }) - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/hello", - "nr.apdexPerfZone": "F", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": "418", - "request.method": "GET", - "request.uri": "/hello", - }, - UserAttributes: map[string]interface{}{}, - }}) -} - -func TestReturnsError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - 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: "hello", - IsWeb: true, - NumErrors: 1, - }) - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/hello", - "nr.apdexPerfZone": "F", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": "500", - "request.method": "GET", - "request.uri": "/hello", - }, - UserAttributes: map[string]interface{}{}, - }}) -} - -func TestResponseCode(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - - e := echo.New() - e.Use(Middleware(app)) - 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: "hello", - IsWeb: true, - NumErrors: 1, - }) - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/hello", - "nr.apdexPerfZone": "F", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": "418", - "request.method": "GET", - "response.headers.contentType": "text/html", - "request.uri": "/hello", - }, - UserAttributes: map[string]interface{}{}, - }}) -} diff --git a/_integrations/nrgin/v1/README.md b/_integrations/nrgin/v1/README.md deleted file mode 100644 index f7e561669..000000000 --- a/_integrations/nrgin/v1/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrgin/v1 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1) - -Package `nrgin` instruments https://github.com/gin-gonic/gin applications. - -```go -import "github.com/newrelic/go-agent/_integrations/nrgin/v1" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgin/v1). diff --git a/_integrations/nrgin/v1/example/main.go b/_integrations/nrgin/v1/example/main.go deleted file mode 100644 index cc4169482..000000000 --- a/_integrations/nrgin/v1/example/main.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "os" - - "github.com/gin-gonic/gin" - "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgin/v1" -) - -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) { - if txn := nrgin.Transaction(c); nil != txn { - txn.SetName("custom-name") - } - c.Writer.WriteString("changed the name of the transaction!") -} - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Gin App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - fmt.Println(err) - os.Exit(1) - } - - router := gin.Default() - router.Use(nrgin.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(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") -} diff --git a/_integrations/nrgin/v1/nrgin.go b/_integrations/nrgin/v1/nrgin.go deleted file mode 100644 index e52fa4469..000000000 --- a/_integrations/nrgin/v1/nrgin.go +++ /dev/null @@ -1,123 +0,0 @@ -// 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/_integrations/nrgin/v1/example/main.go -package nrgin - -import ( - "net/http" - - "github.com/gin-gonic/gin" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -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 - txn newrelic.Transaction - code int - written bool -} - -var _ gin.ResponseWriter = &replacementResponseWriter{} - -func (w *replacementResponseWriter) flushHeader() { - if !w.written { - w.txn.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() - return w.ResponseWriter.Write(data) -} - -func (w *replacementResponseWriter) WriteString(s string) (int, error) { - w.flushHeader() - 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 -} - -// Middleware creates a Gin middleware that instruments requests. -// -// router := gin.Default() -// // Add the nrgin middleware before other middlewares or routes: -// router.Use(nrgin.Middleware(app)) -// -func Middleware(app newrelic.Application) gin.HandlerFunc { - return func(c *gin.Context) { - if app != nil { - name := c.HandlerName() - w := &headerResponseWriter{w: c.Writer} - txn := app.StartTransaction(name, w, c.Request) - defer txn.End() - - repl := &replacementResponseWriter{ - ResponseWriter: c.Writer, - txn: txn, - code: http.StatusOK, - } - c.Writer = repl - defer repl.flushHeader() - - c.Set(internal.GinTransactionContextKey, txn) - } - c.Next() - } -} diff --git a/_integrations/nrgin/v1/nrgin_context_test.go b/_integrations/nrgin/v1/nrgin_context_test.go deleted file mode 100644 index 5d27ae864..000000000 --- a/_integrations/nrgin/v1/nrgin_context_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package nrgin - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -func accessTransactionContextContext(c *gin.Context) { - var ctx context.Context = c - // Transaction is designed to take both a context.Context and a - // *gin.Context. - if txn := Transaction(ctx); nil != txn { - txn.NoticeError(errors.New("problem")) - } - c.Writer.WriteString("accessTransactionContextContext") -} - -func TestContextContextTransaction(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/txn", accessTransactionContextContext) - - 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: pkg + ".accessTransactionContextContext", - IsWeb: true, - NumErrors: 1, - }) -} - -func accessTransactionFromContext(c *gin.Context) { - // This tests that FromContext will find the transaction added to a - // *gin.Context and by nrgin.Middleware. - if txn := newrelic.FromContext(c); nil != txn { - txn.NoticeError(errors.New("problem")) - } - c.Writer.WriteString("accessTransactionFromContext") -} - -func TestFromContext(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/txn", accessTransactionFromContext) - - 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: pkg + ".accessTransactionFromContext", - IsWeb: true, - NumErrors: 1, - }) -} - -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", nil, nil) - 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, - }) -} diff --git a/_integrations/nrgin/v1/nrgin_test.go b/_integrations/nrgin/v1/nrgin_test.go deleted file mode 100644 index dae9c9629..000000000 --- a/_integrations/nrgin/v1/nrgin_test.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgin - -import ( - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -var ( - pkg = "github.com/newrelic/go-agent/_integrations/nrgin/v1" -) - -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)) - 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) - } - app.ExpectTxnMetrics(t, internal.WantTxn{ - Name: pkg + ".hello", - IsWeb: true, - }) -} - -func TestRouterGroup(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - group := router.Group("/group") - group.GET("/hello", 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: pkg + ".hello", - IsWeb: true, - }) -} - -func TestAnonymousHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/anon", func(c *gin.Context) { - c.Writer.WriteString("anonymous function handler") - }) - - 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: pkg + ".TestAnonymousHandler.func1", - IsWeb: 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)) - router.GET("/header", multipleWriteHeader) - - 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: pkg + ".multipleWriteHeader", - IsWeb: true, - NumErrors: 1, - }) -} - -func accessTransactionGinContext(c *gin.Context) { - if txn := Transaction(c); nil != txn { - txn.NoticeError(errors.New("problem")) - } - c.Writer.WriteString("accessTransactionGinContext") -} - -func TestContextTransaction(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/txn", accessTransactionGinContext) - - 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: pkg + ".accessTransactionGinContext", - IsWeb: true, - NumErrors: 1, - }) -} - -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") -} - -func TestStatusCodes(t *testing.T) { - // Test that we are correctly able to collect status code. - // This behavior changed with this pull request: https://github.com/gin-gonic/gin/pull/1606 - // In Gin v1.4.0 and below, we always recorded a 200 status, whereas with - // newer Gin versions we now correctly capture the status. - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/err", errorStatus) - - 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": "WebTransaction/Go/" + pkg + ".errorStatus", - "nr.apdexPerfZone": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 500, - "request.method": "GET", - "request.uri": "/err", - "response.headers.contentType": "text/plain; charset=utf-8", - }, - }}) -} - -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. - app := integrationsupport.NewBasicTestApp() - router := gin.Default() - router.Use(Middleware(app)) - router.GET("/nobody", 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": "WebTransaction/Go/" + pkg + ".noBody", - "nr.apdexPerfZone": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 500, - "request.method": "GET", - "request.uri": "/nobody", - }, - }}) -} diff --git a/_integrations/nrgorilla/v1/README.md b/_integrations/nrgorilla/v1/README.md deleted file mode 100644 index b87202920..000000000 --- a/_integrations/nrgorilla/v1/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrgorilla/v1 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1) - -Package `nrgorilla` instruments https://github.com/gorilla/mux applications. - -```go -import "github.com/newrelic/go-agent/_integrations/nrgorilla/v1" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgorilla/v1). diff --git a/_integrations/nrgorilla/v1/example/main.go b/_integrations/nrgorilla/v1/example/main.go deleted file mode 100644 index afe2f2b44..000000000 --- a/_integrations/nrgorilla/v1/example/main.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - nrgorilla "github.com/newrelic/go-agent/_integrations/nrgorilla/v1" -) - -func makeHandler(text string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(text)) - }) -} - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Gorilla App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - fmt.Println(err) - os.Exit(1) - } - - r := mux.NewRouter() - 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 will be instrumented if it is set. - r.NotFoundHandler = makeHandler("not found") - - http.ListenAndServe(":8000", nrgorilla.InstrumentRoutes(r, app)) -} diff --git a/_integrations/nrgorilla/v1/nrgorilla.go b/_integrations/nrgorilla/v1/nrgorilla.go deleted file mode 100644 index 3050eb8ed..000000000 --- a/_integrations/nrgorilla/v1/nrgorilla.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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. Call nrgorilla.InstrumentRoutes on your gorilla mux.Router -// after your routes have been added to it. -// -// Example: https://github.com/newrelic/go-agent/tree/master/_integrations/nrgorilla/v1/example/main.go -package nrgorilla - -import ( - "net/http" - - "github.com/gorilla/mux" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -func init() { internal.TrackUsage("integration", "framework", "gorilla", "v1") } - -type instrumentedHandler struct { - name string - app newrelic.Application - orig http.Handler -} - -func (h instrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - txn := h.app.StartTransaction(h.name, w, r) - defer txn.End() - - r = newrelic.RequestWithTransactionContext(r, txn) - - h.orig.ServeHTTP(txn, r) -} - -func instrumentRoute(h http.Handler, app newrelic.Application, name string) http.Handler { - if _, ok := h.(instrumentedHandler); ok { - return h - } - return instrumentedHandler{ - name: name, - orig: h, - app: app, - } -} - -func routeName(route *mux.Route) string { - if nil == route { - return "" - } - if n := route.GetName(); n != "" { - return n - } - if n, _ := route.GetPathTemplate(); n != "" { - return n - } - n, _ := route.GetHostTemplate() - return n -} - -// InstrumentRoutes instruments requests through the provided mux.Router. Use -// this after the routes have been added to the router. -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, routeName(route)) - route.Handler(h) - return nil - }) - if nil != r.NotFoundHandler { - r.NotFoundHandler = instrumentRoute(r.NotFoundHandler, app, "NotFoundHandler") - } - } - return r -} diff --git a/_integrations/nrgorilla/v1/nrgorilla_test.go b/_integrations/nrgorilla/v1/nrgorilla_test.go deleted file mode 100644 index afaeb40fb..000000000 --- a/_integrations/nrgorilla/v1/nrgorilla_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgorilla - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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) - 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: "alpha", - IsWeb: 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) - 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: "users/add", - IsWeb: 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) - 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, - }) -} - -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) - InstrumentRoutes(r, app) - 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, - }) -} - -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) - } -} diff --git a/_integrations/nrgrpc/README.md b/_integrations/nrgrpc/README.md deleted file mode 100644 index 0243fc008..000000000 --- a/_integrations/nrgrpc/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrgrpc [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgrpc?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgrpc) - -Package `nrgrpc` instruments https://github.com/grpc/grpc-go. - -```go -import "github.com/newrelic/go-agent/_integrations/nrgrpc" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrgrpc). diff --git a/_integrations/nrgrpc/example/client/client.go b/_integrations/nrgrpc/example/client/client.go deleted file mode 100644 index 3fdff8630..000000000 --- a/_integrations/nrgrpc/example/client/client.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "fmt" - "io" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgrpc" - sampleapp "github.com/newrelic/go-agent/_integrations/nrgrpc/example/sampleapp" - "google.golang.org/grpc" -) - -func doUnaryUnary(ctx context.Context, client sampleapp.SampleApplicationClient) { - msg, err := client.DoUnaryUnary(ctx, &sampleapp.Message{Text: "Hello DoUnaryUnary"}) - if nil != err { - 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 nil != err { - panic(err) - } - for { - msg, err := stream.Recv() - if err == io.EOF { - break - } - if nil != err { - panic(err) - } - fmt.Println(msg.Text) - } -} - -func doStreamUnary(ctx context.Context, client sampleapp.SampleApplicationClient) { - stream, err := client.DoStreamUnary(ctx) - if nil != err { - panic(err) - } - for i := 0; i < 3; i++ { - if err := stream.Send(&sampleapp.Message{Text: "Hello DoStreamUnary"}); nil != err { - if err == io.EOF { - break - } - panic(err) - } - } - msg, err := stream.CloseAndRecv() - if nil != err { - panic(err) - } - fmt.Println(msg.Text) -} - -func doStreamStream(ctx context.Context, client sampleapp.SampleApplicationClient) { - stream, err := client.DoStreamStream(ctx) - if nil != err { - 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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("gRPC Client", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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("main", nil, nil) - 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) -} diff --git a/_integrations/nrgrpc/example/sampleapp/sampleapp.pb.go b/_integrations/nrgrpc/example/sampleapp/sampleapp.pb.go deleted file mode 100644 index 82a38585c..000000000 --- a/_integrations/nrgrpc/example/sampleapp/sampleapp.pb.go +++ /dev/null @@ -1,369 +0,0 @@ -// 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", -} diff --git a/_integrations/nrgrpc/example/sampleapp/sampleapp.proto b/_integrations/nrgrpc/example/sampleapp/sampleapp.proto deleted file mode 100644 index 8ed50752b..000000000 --- a/_integrations/nrgrpc/example/sampleapp/sampleapp.proto +++ /dev/null @@ -1,15 +0,0 @@ -// 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; -} diff --git a/_integrations/nrgrpc/example/server/server.go b/_integrations/nrgrpc/example/server/server.go deleted file mode 100644 index dc5c90e38..000000000 --- a/_integrations/nrgrpc/example/server/server.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - fmt "fmt" - "io" - "net" - "os" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgrpc" - sampleapp "github.com/newrelic/go-agent/_integrations/nrgrpc/example/sampleapp" - "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.StartSegment(newrelic.FromContext(ctx), "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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("gRPC Server", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - panic(err) - } - - lis, err := net.Listen("tcp", "localhost:8080") - if err != nil { - panic(err) - } - grpcServer := grpc.NewServer( - // Add the New Relic gRPC server instrumentation - grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), - grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), - ) - sampleapp.RegisterSampleApplicationServer(grpcServer, &Server{}) - grpcServer.Serve(lis) -} diff --git a/_integrations/nrgrpc/nrgrpc_client.go b/_integrations/nrgrpc/nrgrpc_client.go deleted file mode 100644 index 20be96bc6..000000000 --- a/_integrations/nrgrpc/nrgrpc_client.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgrpc - -import ( - "context" - "io" - "net/url" - "strings" - - newrelic "github.com/newrelic/go-agent" - "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, - } -} - -// 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 - if txn := newrelic.FromContext(ctx); nil != txn { - seg = newrelic.StartExternalSegment(txn, nil) - - method = strings.TrimPrefix(method, "/") - seg.Host = getURL(method, target).Host - seg.Library = "gRPC" - seg.Procedure = method - - payload := txn.CreateDistributedTracePayload() - if txt := payload.Text(); "" != txt { - md, ok := metadata.FromOutgoingContext(ctx) - if !ok { - md = metadata.New(nil) - } - md.Set(newrelic.DistributedTracePayloadHeader, txt) - 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/_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/_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 -} diff --git a/_integrations/nrgrpc/nrgrpc_client_test.go b/_integrations/nrgrpc/nrgrpc_client_test.go deleted file mode 100644 index 2ab77ffba..000000000 --- a/_integrations/nrgrpc/nrgrpc_client_test.go +++ /dev/null @@ -1,597 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgrpc - -import ( - "context" - "encoding/json" - "io" - "testing" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgrpc/testapp" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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, configFn) -} - -var replyFn = func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.AccountID = "123" - reply.TrustedAccountKey = "123" - reply.PrimaryAppID = "456" -} - -var configFn = func(cfg *newrelic.Config) { - cfg.Enabled = false - cfg.DistributedTracer.Enabled = true - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 -} - -func TestUnaryClientInterceptor(t *testing.T) { - app := testApp() - txn := app.StartTransaction("UnaryUnary", nil, nil) - 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 nil != err { - t.Fatal("client call to DoUnaryUnary failed", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/UnaryUnary", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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", nil, nil) - 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 nil != err { - t.Fatal("client call to DoUnaryStream failed", err) - } - var recved int - for { - msg, err := stream.Recv() - if err == io.EOF { - break - } - if nil != err { - t.Fatal("error receiving message", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/UnaryStream", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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", nil, nil) - 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 nil != err { - t.Fatal("client call to DoStreamUnary failed", err) - } - for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { - if err == io.EOF { - break - } - t.Fatal("failure to Send", err) - } - } - msg, err := stream.CloseAndRecv() - if nil != err { - t.Fatal("failure to CloseAndRecv", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/StreamUnary", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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", nil, nil) - 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 nil != err { - t.Fatal("client call to DoStreamStream failed", err) - } - waitc := make(chan struct{}) - go func() { - defer close(waitc) - var recved int - for { - msg, err := stream.Recv() - if err == io.EOF { - break - } - if err != nil { - t.Fatal("failure to Recv", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { - 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) - } - }() - 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() - <-waitc - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/StreamStream", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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", nil, nil) - 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 nil != err { - t.Fatal("client call to DoUnaryUnary failed", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { - t.Fatal("cannot unmarshall client response", err) - } - if hdr, ok := hdrs["newrelic"]; !ok || len(hdr) != 1 || "" == hdr[0] || "payload" == hdr[0] { - t.Error("distributed trace header not sent", hdrs) - } - if hdr, ok := hdrs["testing"]; !ok || len(hdr) != 1 || "hello world" != hdr[0] { - 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 nil != err { - t.Fatal("client call to DoUnaryUnary failed", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(resp.Text), &hdrs) - if nil != err { - 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 nil != err { - t.Fatal("client call to DoStreamUnary failed", err) - } - for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { - if err == io.EOF { - break - } - t.Fatal("failure to Send", err) - } - } - msg, err := stream.CloseAndRecv() - if nil != err { - t.Fatal("failure to CloseAndRecv", err) - } - var hdrs map[string][]string - err = json.Unmarshal([]byte(msg.Text), &hdrs) - if nil != err { - 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", nil, nil) - - 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 nil == err { - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "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{}, - }}, - }, - }}) -} diff --git a/_integrations/nrgrpc/nrgrpc_doc.go b/_integrations/nrgrpc/nrgrpc_doc.go deleted file mode 100644 index 293fa2cec..000000000 --- a/_integrations/nrgrpc/nrgrpc_doc.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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. -// -// To instrument a gRPC server, use UnaryServerInterceptor and -// StreamServerInterceptor with your newrelic.Application to create server -// interceptors to pass to grpc.NewServer. Example: -// -// -// cfg := newrelic.NewConfig("gRPC Server", os.Getenv("NEW_RELIC_LICENSE_KEY")) -// app, _ := newrelic.NewApplication(cfg) -// server := grpc.NewServer( -// grpc.UnaryInterceptor(nrgrpc.UnaryServerInterceptor(app)), -// grpc.StreamInterceptor(nrgrpc.StreamServerInterceptor(app)), -// ) -// -// 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. -// -// Full server example: -// https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/server/server.go -// -// 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. -// -// Full client example: -// https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/client/client.go -package nrgrpc - -import "github.com/newrelic/go-agent/internal" - -func init() { internal.TrackUsage("integration", "framework", "grpc") } diff --git a/_integrations/nrgrpc/nrgrpc_server.go b/_integrations/nrgrpc/nrgrpc_server.go deleted file mode 100644 index edc8d8515..000000000 --- a/_integrations/nrgrpc/nrgrpc_server.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgrpc - -import ( - "context" - "net/http" - "strings" - - newrelic "github.com/newrelic/go-agent" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -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) - - webReq := newrelic.NewStaticWebRequest(hdrs, url, method, newrelic.TransportHTTP) - txn := app.StartTransaction(method, nil, nil) - txn.SetWebRequest(webReq) - - return txn -} - -// 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: -// -// cfg := newrelic.NewConfig("gRPC Server", os.Getenv("NEW_RELIC_LICENSE_KEY")) -// app, _ := newrelic.NewApplication(cfg) -// 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. -// -// Full example: -// https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/server/server.go -// -func UnaryServerInterceptor(app newrelic.Application) grpc.UnaryServerInterceptor { - if nil == app { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return handler(ctx, req) - } - } - - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - txn := startTransaction(ctx, app, info.FullMethod) - defer txn.End() - - ctx = newrelic.NewContext(ctx, txn) - resp, err = handler(ctx, req) - txn.WriteHeader(int(status.Code(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 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. -// -// Example: -// -// cfg := newrelic.NewConfig("gRPC Server", os.Getenv("NEW_RELIC_LICENSE_KEY")) -// app, _ := newrelic.NewApplication(cfg) -// 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. -// -// Full example: -// https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/server/server.go -// -func StreamServerInterceptor(app newrelic.Application) grpc.StreamServerInterceptor { - if nil == app { - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - return handler(srv, ss) - } - } - - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - txn := startTransaction(ss.Context(), app, info.FullMethod) - defer txn.End() - - err := handler(srv, newWrappedServerStream(ss, txn)) - txn.WriteHeader(int(status.Code(err))) - return err - } -} diff --git a/_integrations/nrgrpc/nrgrpc_server_test.go b/_integrations/nrgrpc/nrgrpc_server_test.go deleted file mode 100644 index 806e2dd3e..000000000 --- a/_integrations/nrgrpc/nrgrpc_server_test.go +++ /dev/null @@ -1,607 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrgrpc - -import ( - "context" - "io" - "net" - "strings" - "testing" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgrpc/testapp" - "github.com/newrelic/go-agent/internal" - "google.golang.org/grpc" - "google.golang.org/grpc/test/bufconn" -) - -// 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(string, time.Duration) (net.Conn, error) { - return lis.Dial() - } - conn, err := grpc.Dial("bufnet", - grpc.WithDialer(bufDialer), - grpc.WithInsecure(), - 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 TestUnaryServerInterceptor(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - txn := app.StartTransaction("client", nil, nil) - ctx := newrelic.NewContext(context.Background(), txn) - _, err := client.DoUnaryUnary(ctx, &testapp.Message{}) - if nil != err { - 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/DistributedTrace/AcceptPayload/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, - "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": "WebTransaction/Go/TestApplication/DoUnaryUnary", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/DoUnaryUnary", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) -} - -func TestUnaryServerInterceptorError(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - _, err := client.DoUnaryUnaryError(context.Background(), &testapp.Message{}) - if nil == err { - 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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/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{}{}, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "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": "15", - "error.message": "response code 15", - "guid": internal.MatchAnything, - "priority": internal.MatchAnything, - "sampled": internal.MatchAnything, - "traceId": internal.MatchAnything, - "transactionName": "WebTransaction/Go/TestApplication/DoUnaryUnaryError", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "request.headers.User-Agent": internal.MatchAnything, - "request.headers.contentType": "application/grpc", - "request.method": "TestApplication/DoUnaryUnaryError", - "request.uri": "grpc://bufnet/TestApplication/DoUnaryUnaryError", - }, - UserAttributes: map[string]interface{}{}, - }}) -} - -func TestUnaryStreamServerInterceptor(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - txn := app.StartTransaction("client", nil, nil) - ctx := newrelic.NewContext(context.Background(), txn) - stream, err := client.DoUnaryStream(ctx, &testapp.Message{}) - if nil != err { - t.Fatal("client call to DoUnaryStream failed", err) - } - var recved int - for { - _, err := stream.Recv() - if err == io.EOF { - break - } - if nil != err { - 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/DistributedTrace/AcceptPayload/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, - "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": "WebTransaction/Go/TestApplication/DoUnaryStream", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/DoUnaryStream", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) -} - -func TestStreamUnaryServerInterceptor(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - txn := app.StartTransaction("client", nil, nil) - ctx := newrelic.NewContext(context.Background(), txn) - stream, err := client.DoStreamUnary(ctx) - if nil != err { - t.Fatal("client call to DoStreamUnary failed", err) - } - for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { - if err == io.EOF { - break - } - t.Fatal("failure to Send", err) - } - } - _, err = stream.CloseAndRecv() - if nil != err { - 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/DistributedTrace/AcceptPayload/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, - "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": "WebTransaction/Go/TestApplication/DoStreamUnary", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/DoStreamUnary", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) -} - -func TestStreamStreamServerInterceptor(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - txn := app.StartTransaction("client", nil, nil) - ctx := newrelic.NewContext(context.Background(), txn) - stream, err := client.DoStreamStream(ctx) - if nil != err { - t.Fatal("client call to DoStreamStream failed", err) - } - waitc := make(chan struct{}) - go func() { - defer close(waitc) - var recved int - for { - _, err := stream.Recv() - if err == io.EOF { - break - } - if err != nil { - t.Fatal("failure to Recv", err) - } - recved++ - } - if recved != 3 { - t.Fatal("received incorrect number of messages from server", recved) - } - }() - 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() - <-waitc - - 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/DistributedTrace/AcceptPayload/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, - "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": "WebTransaction/Go/TestApplication/DoStreamStream", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/DoStreamStream", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) -} - -func TestStreamServerInterceptorError(t *testing.T) { - app := testApp() - - s, conn := newTestServerAndConn(t, app) - defer s.Stop() - defer conn.Close() - - client := testapp.NewTestApplicationClient(conn) - stream, err := client.DoUnaryStreamError(context.Background(), &testapp.Message{}) - if nil != err { - t.Fatal("client call to DoUnaryStream failed", err) - } - _, err = stream.Recv() - if nil == err { - 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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/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{}{}, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "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": "15", - "error.message": "response code 15", - "guid": internal.MatchAnything, - "priority": internal.MatchAnything, - "sampled": internal.MatchAnything, - "traceId": internal.MatchAnything, - "transactionName": "WebTransaction/Go/TestApplication/DoUnaryStreamError", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 15, - "request.headers.User-Agent": internal.MatchAnything, - "request.headers.contentType": "application/grpc", - "request.method": "TestApplication/DoUnaryStreamError", - "request.uri": "grpc://bufnet/TestApplication/DoUnaryStreamError", - }, - UserAttributes: map[string]interface{}{}, - }}) -} - -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 nil != err { - 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 nil != err { - t.Fatal("client call to DoStreamUnary failed", err) - } - for i := 0; i < 3; i++ { - if err := stream.Send(&testapp.Message{Text: "Hello DoStreamUnary"}); nil != err { - if err == io.EOF { - break - } - t.Fatal("failure to Send", err) - } - } - msg, err := stream.CloseAndRecv() - if nil != err { - t.Fatal("failure to CloseAndRecv", err) - } - if !strings.Contains(msg.Text, "content-type") { - t.Error("incorrect message received") - } -} - -func TestInterceptorsNilAppReturnNonNil(t *testing.T) { - uInt := UnaryServerInterceptor(nil) - if uInt == nil { - t.Error("UnaryServerInterceptor returned nil") - } - - sInt := StreamServerInterceptor(nil) - if sInt == nil { - t.Error("StreamServerInterceptor returned nil") - } -} diff --git a/_integrations/nrgrpc/testapp/README.md b/_integrations/nrgrpc/testapp/README.md deleted file mode 100644 index 81d7151e8..000000000 --- a/_integrations/nrgrpc/testapp/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# 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 `_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 -``` diff --git a/_integrations/nrgrpc/testapp/server.go b/_integrations/nrgrpc/testapp/server.go deleted file mode 100644 index dcbccc1ed..000000000 --- a/_integrations/nrgrpc/testapp/server.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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" - 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.StartSegment(newrelic.FromContext(ctx), "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.StartSegment(newrelic.FromContext(stream.Context()), "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.StartSegment(newrelic.FromContext(stream.Context()), "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.StartSegment(newrelic.FromContext(stream.Context()), "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() -} diff --git a/_integrations/nrgrpc/testapp/testapp.pb.go b/_integrations/nrgrpc/testapp/testapp.pb.go deleted file mode 100644 index d95c75b24..000000000 --- a/_integrations/nrgrpc/testapp/testapp.pb.go +++ /dev/null @@ -1,469 +0,0 @@ -// 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" - 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_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 -} - -// UnimplementedTestApplicationServer can be embedded to have forward compatible implementations. -type UnimplementedTestApplicationServer struct { -} - -func (*UnimplementedTestApplicationServer) DoUnaryUnary(ctx context.Context, req *Message) (*Message, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoUnaryUnary not implemented") -} -func (*UnimplementedTestApplicationServer) DoUnaryStream(req *Message, srv TestApplication_DoUnaryStreamServer) error { - return status.Errorf(codes.Unimplemented, "method DoUnaryStream not implemented") -} -func (*UnimplementedTestApplicationServer) DoStreamUnary(srv TestApplication_DoStreamUnaryServer) error { - return status.Errorf(codes.Unimplemented, "method DoStreamUnary not implemented") -} -func (*UnimplementedTestApplicationServer) DoStreamStream(srv TestApplication_DoStreamStreamServer) error { - return status.Errorf(codes.Unimplemented, "method DoStreamStream not implemented") -} -func (*UnimplementedTestApplicationServer) DoUnaryUnaryError(ctx context.Context, req *Message) (*Message, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoUnaryUnaryError not implemented") -} -func (*UnimplementedTestApplicationServer) DoUnaryStreamError(req *Message, srv TestApplication_DoUnaryStreamErrorServer) error { - return status.Errorf(codes.Unimplemented, "method DoUnaryStreamError not implemented") -} - -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", -} diff --git a/_integrations/nrgrpc/testapp/testapp.proto b/_integrations/nrgrpc/testapp/testapp.proto deleted file mode 100644 index 99f9aecb5..000000000 --- a/_integrations/nrgrpc/testapp/testapp.proto +++ /dev/null @@ -1,18 +0,0 @@ -// 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; -} diff --git a/_integrations/nrhttprouter/README.md b/_integrations/nrhttprouter/README.md deleted file mode 100644 index 8570c3b7b..000000000 --- a/_integrations/nrhttprouter/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrhttprouter [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter) - -Package `nrhttprouter` instruments https://github.com/julienschmidt/httprouter applications. - -```go -import "github.com/newrelic/go-agent/_integrations/nrhttprouter" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrhttprouter). diff --git a/_integrations/nrhttprouter/example/main.go b/_integrations/nrhttprouter/example/main.go deleted file mode 100644 index b359f7e17..000000000 --- a/_integrations/nrhttprouter/example/main.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrhttprouter" -) - -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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("httprouter App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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) -} diff --git a/_integrations/nrhttprouter/nrhttprouter.go b/_integrations/nrhttprouter/nrhttprouter.go deleted file mode 100644 index 5c29b46f3..000000000 --- a/_integrations/nrhttprouter/nrhttprouter.go +++ /dev/null @@ -1,151 +0,0 @@ -// 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" -// "github.com/newrelic/go-agent/_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/_integrations/nrhttprouter/example/main.go -package nrhttprouter - -import ( - "net/http" - - "github.com/julienschmidt/httprouter" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -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), w, req) - defer txn.End() - - req = newrelic.RequestWithTransactionContext(req, txn) - - original(txn, req, ps) - } - } - r.Router.Handle(method, path, handle) -} - -// 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, txnName(method, 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) { - if nil != r.application { - h, _, _ := r.Router.Lookup(req.Method, req.URL.Path) - if nil == h { - txn := r.application.StartTransaction("NotFound", w, req) - defer txn.End() - w = txn - req = newrelic.RequestWithTransactionContext(req, txn) - } - } - - r.Router.ServeHTTP(w, req) -} diff --git a/_integrations/nrhttprouter/nrhttprouter_test.go b/_integrations/nrhttprouter/nrhttprouter_test.go deleted file mode 100644 index 2b853fe1f..000000000 --- a/_integrations/nrhttprouter/nrhttprouter_test.go +++ /dev/null @@ -1,286 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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.NewBasicTestApp() - router := New(app) - 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, - }) - } -} - -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.NewBasicTestApp() - router := New(app) - - 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")))) - if txn := newrelic.FromContext(r.Context()); txn != nil { - 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, - }) - app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/GET /hello/:name", - "nr.apdexPerfZone": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{ - "color": "purple", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 500, - "request.method": "GET", - "request.uri": "/hello/person", - }, - }, - }) -} - -func TestHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := New(app) - - 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")) - if txn := newrelic.FromContext(r.Context()); txn != nil { - 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, - }) - app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/GET /hello/", - "nr.apdexPerfZone": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{ - "color": "purple", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 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.NewBasicTestApp() - router := New(app) - - 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, - }) -} - -func TestNotFound(t *testing.T) { - app := integrationsupport.NewBasicTestApp() - router := New(app) - - 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!")) - if txn := newrelic.FromContext(r.Context()); txn != nil { - 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, - }) - app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/NotFound", - "nr.apdexPerfZone": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{ - "color": "purple", - }, - AgentAttributes: map[string]interface{}{ - "httpResponseCode": 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.NewBasicTestApp() - router := New(app) - - 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, - }) -} diff --git a/_integrations/nrlambda/README.md b/_integrations/nrlambda/README.md deleted file mode 100644 index b70a5d05a..000000000 --- a/_integrations/nrlambda/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrlambda [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlambda?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlambda) - -Package `nrlambda` adds support for AWS Lambda. - -```go -import "github.com/newrelic/go-agent/_integrations/nrlambda" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlambda). diff --git a/_integrations/nrlambda/config.go b/_integrations/nrlambda/config.go deleted file mode 100644 index 1fd44c4b6..000000000 --- a/_integrations/nrlambda/config.go +++ /dev/null @@ -1,42 +0,0 @@ -// 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" -) - -// NewConfig populates a newrelic.Config with correct default settings for a -// Lambda serverless environment. NewConfig 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 NewConfig() newrelic.Config { - return newConfigInternal(os.Getenv) -} - -func newConfigInternal(getenv func(string) string) newrelic.Config { - cfg := newrelic.NewConfig("", "") - - 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 - } - } - - return cfg -} diff --git a/_integrations/nrlambda/config_test.go b/_integrations/nrlambda/config_test.go deleted file mode 100644 index 8273c5e9d..000000000 --- a/_integrations/nrlambda/config_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrlambda - -import ( - "testing" - "time" -) - -func TestNewConfig(t *testing.T) { - cfg := 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 "" - } - }) - 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) - } -} diff --git a/_integrations/nrlambda/events.go b/_integrations/nrlambda/events.go deleted file mode 100644 index e85d54d6c..000000000 --- a/_integrations/nrlambda/events.go +++ /dev/null @@ -1,127 +0,0 @@ -// 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" -) - -func getEventSourceARN(event interface{}) string { - switch v := event.(type) { - case events.KinesisFirehoseEvent: - return v.DeliveryStreamArn - case events.KinesisEvent: - if len(v.Records) > 0 { - return v.Records[0].EventSourceArn - } - case events.CodeCommitEvent: - if len(v.Records) > 0 { - return v.Records[0].EventSourceARN - } - case events.DynamoDBEvent: - if len(v.Records) > 0 { - return v.Records[0].EventSourceArn - } - case events.SQSEvent: - if len(v.Records) > 0 { - return v.Records[0].EventSourceARN - } - case events.S3Event: - if len(v.Records) > 0 { - return v.Records[0].S3.Bucket.Arn - } - case events.SNSEvent: - if len(v.Records) > 0 { - return v.Records[0].EventSubscriptionArn - } - } - return "" -} - -type webRequest struct { - header http.Header - method string - u *url.URL - transport newrelic.TransportType -} - -func (r webRequest) Header() http.Header { return r.header } -func (r webRequest) URL() *url.URL { return r.u } -func (r webRequest) Method() string { return r.method } -func (r webRequest) Transport() newrelic.TransportType { return r.transport } - -func eventWebRequest(event interface{}) newrelic.WebRequest { - var path string - var request 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.ALBTargetGroupRequest: - // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#receive-event-from-load-balancer - request.method = r.HTTPMethod - path = r.Path - headers = r.Headers - 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.u = &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 - - switch r := event.(type) { - case events.APIGatewayProxyResponse: - code = r.StatusCode - headers = r.Headers - case events.ALBTargetGroupResponse: - code = r.StatusCode - headers = r.Headers - default: - return nil - } - hdr := make(http.Header, len(headers)) - for k, v := range headers { - hdr.Add(k, v) - } - return &response{ - code: code, - header: hdr, - } -} diff --git a/_integrations/nrlambda/events_test.go b/_integrations/nrlambda/events_test.go deleted file mode 100644 index 34bc557d4..000000000 --- a/_integrations/nrlambda/events_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrlambda - -import ( - "testing" - - "github.com/aws/aws-lambda-go/events" - newrelic "github.com/newrelic/go-agent" -) - -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: "SNSEvent empty", Input: events.SNSEvent{}, 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: "DynamoDBEvent empty", Input: events.DynamoDBEvent{}, 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: "KinesisEvent empty", Input: events.KinesisEvent{}, Arn: ""}, - {Name: "KinesisEvent", Input: events.KinesisEvent{ - Records: []events.KinesisEventRecord{{ - EventSourceArn: "ARN", - }}, - }, Arn: "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 proxy request", - input: events.APIGatewayProxyRequest{}, - numHeaders: 0, - method: "", - urlString: "", - transport: newrelic.TransportUnknown, - }, - { - testname: "populated proxy request", - 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 alb request", - input: events.ALBTargetGroupRequest{}, - numHeaders: 0, - method: "", - urlString: "", - transport: newrelic.TransportUnknown, - }, - { - testname: "populated alb request", - 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) - } - - testcases := []struct { - testname string - input interface{} - numHeaders int - code int - }{ - { - testname: "empty proxy response", - input: events.APIGatewayProxyResponse{}, - numHeaders: 0, - code: 0, - }, - { - testname: "populated proxy response", - input: events.APIGatewayProxyResponse{ - StatusCode: 200, - Headers: map[string]string{ - "x-custom-header": "my custom header value", - }, - }, - numHeaders: 1, - code: 200, - }, - { - testname: "empty alb response", - input: events.ALBTargetGroupResponse{}, - numHeaders: 0, - code: 0, - }, - { - testname: "populated alb response", - input: events.ALBTargetGroupResponse{ - StatusCode: 200, - Headers: map[string]string{ - "x-custom-header": "my custom header value", - }, - }, - numHeaders: 1, - code: 200, - }, - } - - for _, tc := range testcases { - resp = eventResponse(tc.input) - if resp == nil { - t.Error(tc.testname, "no response returned") - continue - } - if h := resp.Header(); len(h) != tc.numHeaders { - t.Error(tc.testname, "header len mismatch", h, tc.numHeaders) - } - if resp.code != tc.code { - t.Error(tc.testname, "status code mismatch", resp.code, tc.code) - } - } -} diff --git a/_integrations/nrlambda/example/main.go b/_integrations/nrlambda/example/main.go deleted file mode 100644 index a36a3b461..000000000 --- a/_integrations/nrlambda/example/main.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "fmt" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrlambda" -) - -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. - if txn := newrelic.FromContext(ctx); nil != txn { - txn.AddAttribute("userLevel", "gold") - txn.Application().RecordCustomEvent("MyEvent", map[string]interface{}{ - "zip": "zap", - }) - } - fmt.Println("hello world") -} - -func main() { - // nrlambda.NewConfig should be used in place of newrelic.NewConfig - // since it sets Lambda specific configuration settings including - // Config.ServerlessMode.Enabled. - cfg := nrlambda.NewConfig() - // Here is the opportunity to change configuration settings before the - // application is created. - app, err := newrelic.NewApplication(cfg) - 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) -} diff --git a/_integrations/nrlambda/handler.go b/_integrations/nrlambda/handler.go deleted file mode 100644 index 5d52cd748..000000000 --- a/_integrations/nrlambda/handler.go +++ /dev/null @@ -1,162 +0,0 @@ -// 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/_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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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, internal.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 { - txn.SetWebResponse(rw) - txn.WriteHeader(rw.code) - } -} - -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 internal.ServerlessWrite(h.app, arn, h.writer) - - txn := h.app.StartTransaction(h.functionName, nil, nil) - defer txn.End() - - integrationsupport.AddAgentAttribute(txn, internal.AttributeAWSRequestID, requestID, nil) - integrationsupport.AddAgentAttribute(txn, internal.AttributeAWSLambdaARN, arn, nil) - h.firstTransaction.Do(func() { - integrationsupport.AddAgentAttribute(txn, internal.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 - // writer is used to log the data JSON at the end of each transaction. - // This field exists (rather than hardcoded os.Stdout) for testing. - writer io.Writer -} - -// 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, - writer: os.Stdout, - } -} - -// 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)) -} diff --git a/_integrations/nrlambda/handler_test.go b/_integrations/nrlambda/handler_test.go deleted file mode 100644 index 59b4006bc..000000000 --- a/_integrations/nrlambda/handler_test.go +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrlambda - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "strings" - "testing" - - "github.com/aws/aws-lambda-go/events" - "github.com/aws/aws-lambda-go/lambdacontext" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -func dataShouldContain(tb testing.TB, data map[string]json.RawMessage, keys ...string) { - if h, ok := tb.(interface { - Helper() - }); ok { - h.Helper() - } - if len(data) != len(keys) { - tb.Errorf("data key length mismatch, expected=%v got=%v", - len(keys), len(data)) - return - } - for _, k := range keys { - _, ok := data[k] - if !ok { - tb.Errorf("data does not contain key %v", k) - } - } -} - -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) - if nil != err { - t.Fatal(err) - } - internal.HarvestTesting(app, 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 "" - } -} - -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.writer = 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.(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, - }, - }}) - metadata, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") - if v := string(metadata["arn"]); v != `"function-arn"` { - t.Error(metadata) - } - - // Invoke the handler again to test the cold-start attribute absence. - buf = &bytes.Buffer{} - w.writer = buf - internal.HarvestTesting(app, nil) - resp, err = wrapped.Invoke(ctx, nil) - if nil != err || string(resp) != "null" { - t.Error("unexpected response", err, string(resp)) - } - app.(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", - }, - }}) - metadata, data, err = internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") - if v := string(metadata["arn"]); v != `"function-arn"` { - t.Error(metadata) - } -} - -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.writer = buf - - resp, err := wrapped.Invoke(context.Background(), nil) - if err != returnError || string(resp) != "" { - t.Error(err, string(resp)) - } - app.(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.(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, - }, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data", - "error_event_data", "error_data") -} - -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.writer = 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.(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.(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", - }, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") -} - -func makePayload(app newrelic.Application) string { - txn := app.StartTransaction("hello", nil, nil) - return txn.CreateDistributedTracePayload().Text() -} - -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.writer = buf - - req := events.APIGatewayProxyRequest{ - Headers: map[string]string{ - "X-Forwarded-Port": "4000", - "X-Forwarded-Proto": "HTTPS", - newrelic.DistributedTracePayloadHeader: makePayload(app), - }, - } - 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.(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/DistributedTrace/AcceptPayload/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.(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", - }, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") -} - -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.writer = 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.(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.(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", - }, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") -} - -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.writer = 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.(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", - "response.headers.contentType": "text/html", - }, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") -} - -func TestCustomEvent(t *testing.T) { - originalHandler := func(c context.Context) { - if txn := newrelic.FromContext(c); nil != txn { - 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.writer = buf - - resp, err := wrapped.Invoke(context.Background(), nil) - if nil != err || string(resp) != "null" { - t.Error("unexpected response", err, string(resp)) - } - app.(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{}{}, - }}) - _, data, err := internal.ParseServerlessPayload(buf.Bytes()) - if err != nil { - t.Error(err) - } - dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data", "custom_event_data") -} diff --git a/_integrations/nrlogrus/README.md b/_integrations/nrlogrus/README.md deleted file mode 100644 index cfa195a99..000000000 --- a/_integrations/nrlogrus/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrlogrus [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus) - -Package `nrlogrus` sends go-agent log messages to https://github.com/sirupsen/logrus. - -```go -import "github.com/newrelic/go-agent/_integrations/nrlogrus" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus). diff --git a/_integrations/nrlogrus/example/main.go b/_integrations/nrlogrus/example/main.go deleted file mode 100644 index c44da6f96..000000000 --- a/_integrations/nrlogrus/example/main.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "io" - "net/http" - "os" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrlogrus" - "github.com/sirupsen/logrus" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Logrus App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - logrus.SetLevel(logrus.DebugLevel) - cfg.Logger = nrlogrus.StandardLogger() - - app, err := newrelic.NewApplication(cfg) - 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) -} diff --git a/_integrations/nrlogrus/nrlogrus.go b/_integrations/nrlogrus/nrlogrus.go deleted file mode 100644 index 648c1b0e7..000000000 --- a/_integrations/nrlogrus/nrlogrus.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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, assign the newrelic.Config.Logger field to -// nrlogrus.StandardLogger(): -// -// cfg := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__") -// cfg.Logger = nrlogrus.StandardLogger() -// -// If you are using a particular logrus Logger instance, assign the -// newrelic.Config.Logger field to the the output of nrlogrus.Transform: -// -// l := logrus.New() -// l.SetLevel(logrus.DebugLevel) -// cfg := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__") -// cfg.Logger = nrlogrus.Transform(l) -// -// This package requires logrus version v1.1.0 and above. -package nrlogrus - -import ( - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "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", - }), - } -} diff --git a/_integrations/nrlogrus/nrlogrus_test.go b/_integrations/nrlogrus/nrlogrus_test.go deleted file mode 100644 index e9fe3ac33..000000000 --- a/_integrations/nrlogrus/nrlogrus_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrlogrus - -import ( - "bytes" - "strings" - "testing" - - "github.com/sirupsen/logrus" -) - -func bufferToStringAndReset(buf *bytes.Buffer) string { - s := buf.String() - buf.Reset() - return s -} - -func TestLogrus(t *testing.T) { - buf := &bytes.Buffer{} - l := logrus.New() - l.SetOutput(buf) - l.SetLevel(logrus.DebugLevel) - lg := Transform(l) - lg.Debug("elephant", map[string]interface{}{"color": "gray"}) - s := bufferToStringAndReset(buf) - if !strings.Contains(s, "elephant") || !strings.Contains(s, "gray") { - t.Error(s) - } - if enabled := lg.DebugEnabled(); !enabled { - t.Error(enabled) - } - // Now switch the level and test that debug is no longer enabled. - l.SetLevel(logrus.InfoLevel) - lg.Debug("lion", map[string]interface{}{"color": "yellow"}) - s = bufferToStringAndReset(buf) - if strings.Contains(s, "lion") || strings.Contains(s, "yellow") { - t.Error(s) - } - if enabled := lg.DebugEnabled(); enabled { - t.Error(enabled) - } - lg.Info("tiger", map[string]interface{}{"color": "orange"}) - s = bufferToStringAndReset(buf) - if !strings.Contains(s, "tiger") || !strings.Contains(s, "orange") { - t.Error(s) - } -} diff --git a/_integrations/nrlogxi/v1/README.md b/_integrations/nrlogxi/v1/README.md deleted file mode 100644 index 54244d0cc..000000000 --- a/_integrations/nrlogxi/v1/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrlogxi/v1 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogxi/v1?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogxi/v1) - -Package `nrlogxi` supports https://github.com/mgutz/logxi. - -```go -import "github.com/newrelic/go-agent/_integrations/nrlogxi/v1" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogxi/v1). diff --git a/_integrations/nrlogxi/v1/example_test.go b/_integrations/nrlogxi/v1/example_test.go deleted file mode 100644 index d2f959f78..000000000 --- a/_integrations/nrlogxi/v1/example_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrlogxi_test - -import ( - log "github.com/mgutz/logxi/v1" - newrelic "github.com/newrelic/go-agent" - nrlogxi "github.com/newrelic/go-agent/_integrations/nrlogxi/v1" -) - -func Example() { - cfg := newrelic.NewConfig("Example App", "__YOUR_NEWRELIC_LICENSE_KEY__") - - // Create a new logxi logger: - l := log.New("newrelic") - l.SetLevel(log.LevelInfo) - - // Use nrlogxi to register the logger with the agent: - cfg.Logger = nrlogxi.New(l) - - newrelic.NewApplication(cfg) -} diff --git a/_integrations/nrlogxi/v1/nrlogxi.go b/_integrations/nrlogxi/v1/nrlogxi.go deleted file mode 100644 index 85a590542..000000000 --- a/_integrations/nrlogxi/v1/nrlogxi.go +++ /dev/null @@ -1,52 +0,0 @@ -// 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 ( - "github.com/mgutz/logxi/v1" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -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{} { - 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, - } -} diff --git a/_integrations/nrmicro/README.md b/_integrations/nrmicro/README.md deleted file mode 100644 index 371bb12b1..000000000 --- a/_integrations/nrmicro/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrmicro [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro) - -Package `nrmicro` instruments https://github.com/micro/go-micro. - -```go -import "github.com/newrelic/go-agent/_integrations/nrmicro" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmicro). diff --git a/_integrations/nrmicro/example/README.md b/_integrations/nrmicro/example/README.md deleted file mode 100644 index f2981c866..000000000 --- a/_integrations/nrmicro/example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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. - \ No newline at end of file diff --git a/_integrations/nrmicro/example/client/client.go b/_integrations/nrmicro/example/client/client.go deleted file mode 100644 index cc10a5fb5..000000000 --- a/_integrations/nrmicro/example/client/client.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrmicro" - proto "github.com/newrelic/go-agent/_integrations/nrmicro/example/proto" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Micro Client", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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", nil, nil) - 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) -} diff --git a/_integrations/nrmicro/example/proto/greeter.micro.go b/_integrations/nrmicro/example/proto/greeter.micro.go deleted file mode 100644 index 2b25211be..000000000 --- a/_integrations/nrmicro/example/proto/greeter.micro.go +++ /dev/null @@ -1,94 +0,0 @@ -// 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) -} diff --git a/_integrations/nrmicro/example/proto/greeter.pb.go b/_integrations/nrmicro/example/proto/greeter.pb.go deleted file mode 100644 index 0b1ea5f82..000000000 --- a/_integrations/nrmicro/example/proto/greeter.pb.go +++ /dev/null @@ -1,122 +0,0 @@ -// 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, -} diff --git a/_integrations/nrmicro/example/proto/greeter.proto b/_integrations/nrmicro/example/proto/greeter.proto deleted file mode 100644 index dc4ff3b19..000000000 --- a/_integrations/nrmicro/example/proto/greeter.proto +++ /dev/null @@ -1,16 +0,0 @@ -// 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; -} diff --git a/_integrations/nrmicro/example/pubsub/main.go b/_integrations/nrmicro/example/pubsub/main.go deleted file mode 100644 index c6971a198..000000000 --- a/_integrations/nrmicro/example/pubsub/main.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrmicro" - proto "github.com/newrelic/go-agent/_integrations/nrmicro/example/proto" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -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", nil, nil) - 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() { - cfg := newrelic.NewConfig("Micro Pub/Sub", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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) - } -} diff --git a/_integrations/nrmicro/example/server/server.go b/_integrations/nrmicro/example/server/server.go deleted file mode 100644 index f56dfcf96..000000000 --- a/_integrations/nrmicro/example/server/server.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrmicro" - proto "github.com/newrelic/go-agent/_integrations/nrmicro/example/proto" -) - -// 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() - if txn := newrelic.FromContext(ctx); nil != txn { - txn.AddAttribute("Name", name) - } - fmt.Println("Request received from", name) - rsp.Greeting = "Hello " + name - return nil -} - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Micro Server", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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) - } -} diff --git a/_integrations/nrmicro/nrmicro.go b/_integrations/nrmicro/nrmicro.go deleted file mode 100644 index a4ce3319d..000000000 --- a/_integrations/nrmicro/nrmicro.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrmicro - -import ( - "context" - "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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" -) - -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: newrelic.StartSegmentNow(txn), - 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: newrelic.StartSegmentNow(txn), - Library: "Micro", - DestinationType: newrelic.MessageTopic, - DestinationName: topic, - } - ctx = addDTPayloadToContext(ctx, txn) - } - return ctx, seg -} - -func addDTPayloadToContext(ctx context.Context, txn newrelic.Transaction) context.Context { - payload := txn.CreateDistributedTracePayload() - if txt := payload.Text(); "" != txt { - md, _ := metadata.FromContext(ctx) - md = metadata.Copy(md) - md[newrelic.DistributedTracePayloadHeader] = txt - 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() - err := fn(newrelic.NewContext(ctx, txn), req, rsp) - var code int - if err != nil { - if t, ok := err.(*errors.Error); ok { - code = int(t.Code) - } else { - code = 500 - } - } else { - code = 200 - } - txn.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(), nil, nil) - defer txn.End() - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageRoutingKey, m.Topic(), nil) - md, ok := metadata.FromContext(ctx) - if ok { - txn.AcceptDistributedTracePayload(newrelic.TransportHTTP, md[newrelic.DistributedTracePayloadHeader]) - } - 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(), nil, nil) - u := &url.URL{ - Scheme: "micro", - Host: req.Service(), - Path: req.Endpoint(), - } - - webReq := newrelic.NewStaticWebRequest(hdrs, u, req.Method(), newrelic.TransportHTTP) - txn.SetWebRequest(webReq) - - return txn -} diff --git a/_integrations/nrmicro/nrmicro_doc.go b/_integrations/nrmicro/nrmicro_doc.go deleted file mode 100644 index 585e1cf9e..000000000 --- a/_integrations/nrmicro/nrmicro_doc.go +++ /dev/null @@ -1,184 +0,0 @@ -// 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/_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/_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/_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/_integrations/nrmicro/example/pubsub/main.go -package nrmicro - -import "github.com/newrelic/go-agent/internal" - -func init() { internal.TrackUsage("integration", "framework", "micro") } diff --git a/_integrations/nrmicro/nrmicro_test.go b/_integrations/nrmicro/nrmicro_test.go deleted file mode 100644 index 0cd160a6f..000000000 --- a/_integrations/nrmicro/nrmicro_test.go +++ /dev/null @@ -1,1034 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - proto "github.com/newrelic/go-agent/_integrations/nrmicro/example/proto" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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.StartSegment(newrelic.FromContext(ctx), "Method").End() - return nil -} - -func (t *TestHandler) StreamingMethod(ctx context.Context, stream server.Stream) error { - 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.DistributedTracePayloadHeader]; ok { - return dtHeader - } - return missingHeaders - } - return missingMetadata -} - -func createTestApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(replyFn, cfgFn) -} - -var replyFn = func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.AccountID = "123" - reply.TrustedAccountKey = "123" - reply.PrimaryAppID = "456" -} - -var cfgFn = func(cfg *newrelic.Config) { - cfg.Enabled = false - cfg.DistributedTracer.Enabled = true - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - 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(), client.Wrap(ClientWrapper()), t) - defer s.Stop() - testClientCallWithNoTransaction(c, t) -} - -func TestClientCallWrapperWithNoTransaction(t *testing.T) { - c, s := newTestWrappedClientAndServer(createTestApp(), 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(), client.Wrap(ClientWrapper()), t) - defer s.Stop() - testClientCallWithTransaction(c, t) -} - -func TestClientCallWrapperWithTransaction(t *testing.T) { - c, s := newTestWrappedClientAndServer(createTestApp(), 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", nil, nil) - 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/DistributedTrace/CreatePayload/Success", Scope: "", Forced: true, Data: nil}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/name", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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(), client.Wrap(ClientWrapper()), t) - defer s.Stop() - testClientCallMetadata(c, t) -} - -func TestCallMetadata(t *testing.T) { - c, s := newTestWrappedClientAndServer(createTestApp(), 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", nil, nil) - 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(), 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.DistributedTracePayloadHeader]; 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(), 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.DistributedTracePayloadHeader]; !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", nil, nil) - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/name", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "MessageBroker/Micro/Topic/Produce/Named/topic", - "parentId": internal.MatchAnything, - }, - 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(), 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 - 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(), client.Wrap(ClientWrapper()), t) - defer s.Stop() - - app := createTestApp() - txn := app.StartTransaction("name", nil, nil) - 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}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "OtherTransaction/Go/name", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) - 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, client.Wrap(ClientWrapper()), t) - defer s.Stop() - ctx := context.Background() - txn := app.StartTransaction("txn", nil, nil) - 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/DistributedTrace/AcceptPayload/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": "WebTransaction/Go/TestHandler.Method", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/Method", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) - 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", - }, - }}) -} - -func TestServerWrapperWithAppReturnsError(t *testing.T) { - app := createTestApp() - c, s := newTestWrappedClientAndServer(app, 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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/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", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) - 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, - }, - }}) - 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, - "sampled": "true", - }, - }}) -} - -func TestServerWrapperWithAppReturnsNonMicroError(t *testing.T) { - app := createTestApp() - c, s := newTestWrappedClientAndServer(app, 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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/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, - }, - }}) - 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, - "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, t) - - var wg sync.WaitGroup - err := micro.RegisterSubscriber(topic, s, func(ctx context.Context, msg *proto.HelloRequest) error { - txn := newrelic.FromContext(ctx) - defer newrelic.StartSegment(txn, "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", nil, nil) - 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/DistributedTrace/AcceptPayload/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": "OtherTransaction/Go/Message/Micro/Topic/Named/topic", - "nr.entryPoint": true, - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "Custom/segment", - "parentId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) - 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, 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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/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/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/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", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: 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{}, - }}, - }, - }}) - 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, - "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 -} diff --git a/_integrations/nrmongo/README.md b/_integrations/nrmongo/README.md deleted file mode 100644 index 03296bd06..000000000 --- a/_integrations/nrmongo/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrmongo [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmongo?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmongo) - -Package `nrmongo` instruments https://github.com/mongodb/mongo-go-driver - -```go -import "github.com/newrelic/go-agent/_integrations/nrmongo" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmongo). diff --git a/_integrations/nrmongo/example/main.go b/_integrations/nrmongo/example/main.go deleted file mode 100644 index 860725d32..000000000 --- a/_integrations/nrmongo/example/main.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrmongo" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -func main() { - config := newrelic.NewConfig("Basic Mongo Example", os.Getenv("NEW_RELIC_LICENSE_KEY")) - config.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(config) - if nil != err { - 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 - nrMon := nrmongo.NewCommandMonitor(nil) - ctx := context.Background() - - // nrMon 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(nrMon)) - if err != nil { - panic(err) - } - defer client.Disconnect(ctx) - - txn := app.StartTransaction("Mongo txn", nil, nil) - // 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) - -} diff --git a/_integrations/nrmongo/nrmongo.go b/_integrations/nrmongo/nrmongo.go deleted file mode 100644 index 98277841d..000000000 --- a/_integrations/nrmongo/nrmongo.go +++ /dev/null @@ -1,159 +0,0 @@ -// 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): -// -// nrMon := nrmongo.NewCommandMonitor(nil) -// client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon)) -// -// 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, -// } -// nrMon := nrmongo.NewCommandMonitor(origMon) -// client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon)) -// -// Then add the current transaction to the context used in any MongoDB call: -// -// ctx = newrelic.NewContext(context.Background(), txn) -// resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) -package nrmongo - -import ( - "context" - "regexp" - "sync" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "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. -// -// // Use `SetMonitor` to register the CommandMonitor. -// client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017").SetMonitor(nrmongo.NewCommandMonitor(nil))) -// if err != nil { -// log.Fatal(err) -// } -// -// // Add transaction to the context. This step is required. -// ctx = newrelic.NewContext(ctx, txn) -// -// collection := client.Database("testing").Collection("numbers") -// resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159}) -// if err != nil { -// log.Fatal(err) -// } -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) { - if m.origCommMon != nil && m.origCommMon.Started != nil { - m.origCommMon.Started(ctx, e) - } - txn := newrelic.FromContext(ctx) - if txn == nil { - return - } - host, port := calcHostAndPort(e.ConnectionID) - sgmt := newrelic.DatastoreSegment{ - StartTime: newrelic.StartSegmentNow(txn), - Product: newrelic.DatastoreMongoDB, - Collection: collName(e), - Operation: e.CommandName, - Host: host, - PortPathOrID: port, - DatabaseName: e.DatabaseName, - } - 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) { - 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 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 -} diff --git a/_integrations/nrmongo/nrmongo_test.go b/_integrations/nrmongo/nrmongo_test.go deleted file mode 100644 index 9f0ecd8e5..000000000 --- a/_integrations/nrmongo/nrmongo_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrmongo - -import ( - "context" - "testing" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/integrationsupport" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/event" -) - -var ( - 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{ - DurationNanos: 5, - CommandName: "name", - RequestID: reqID, - ConnectionID: connID, - } - se = &event.CommandSucceededEvent{ - CommandFinishedEvent: finishedEvent, - Reply: nil, - } - fe = &event.CommandFailedEvent{ - CommandFinishedEvent: finishedEvent, - Failure: "failureCause", - } -) - -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", nil, nil) - 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)) - } - 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/" + internal.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": "OtherTransaction/Go/txnName", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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": internal.ThisHost + ":27017", - "peer.hostname": internal.ThisHost, - "db.statement": "'commName' on 'collName' using 'MongoDB'", - "db.instance": "testdb", - "db.collection": "collName", - }, - }, - }) - - txn = app.StartTransaction("txnName", nil, nil) - 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)) - } - nrMonitor.failed(ctx, fe) - if !failed { - 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/" + internal.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 createTestApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(replyFn, cfgFn) -} - -var cfgFn = func(cfg *newrelic.Config) { - cfg.Enabled = false - cfg.DistributedTracer.Enabled = true - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 -} - -var replyFn = func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} -} diff --git a/_integrations/nrmysql/README.md b/_integrations/nrmysql/README.md deleted file mode 100644 index c1f8ed267..000000000 --- a/_integrations/nrmysql/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrmysql [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmysql?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmysql) - -Package `nrmysql` instruments https://github.com/go-sql-driver/mysql. - -```go -import "github.com/newrelic/go-agent/_integrations/nrmysql" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrmysql). diff --git a/_integrations/nrmysql/example/main.go b/_integrations/nrmysql/example/main.go deleted file mode 100644 index 63f6666f4..000000000 --- a/_integrations/nrmysql/example/main.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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" - _ "github.com/newrelic/go-agent/_integrations/nrmysql" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -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) - } - - cfg := newrelic.NewConfig("MySQL App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - panic(err) - } - app.WaitForConnection(5 * time.Second) - txn := app.StartTransaction("mysqlQuery", nil, nil) - - 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) -} diff --git a/_integrations/nrmysql/nrmysql.go b/_integrations/nrmysql/nrmysql.go deleted file mode 100644 index 78b2638b5..000000000 --- a/_integrations/nrmysql/nrmysql.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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/_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/_integrations/nrmysql/example/main.go -package nrmysql - -import ( - "database/sql" - "net" - - "github.com/go-sql-driver/mysql" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/sqlparse" -) - -var ( - baseBuilder = newrelic.SQLDriverSegmentBuilder{ - BaseSegment: newrelic.DatastoreSegment{ - Product: newrelic.DatastoreMySQL, - }, - ParseQuery: sqlparse.ParseQuery, - ParseDSN: parseDSN, - } -) - -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 -} diff --git a/_integrations/nrmysql/nrmysql_test.go b/_integrations/nrmysql/nrmysql_test.go deleted file mode 100644 index 7526a8591..000000000 --- a/_integrations/nrmysql/nrmysql_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// 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" -) - -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) - } - } -} diff --git a/_integrations/nrnats/README.md b/_integrations/nrnats/README.md deleted file mode 100644 index be66253f2..000000000 --- a/_integrations/nrnats/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrnats [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats) - -Package `nrnats` instruments https://github.com/nats-io/nats.go. - -```go -import "github.com/newrelic/go-agent/_integrations/nrnats" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrnats). diff --git a/_integrations/nrnats/example_test.go b/_integrations/nrnats/example_test.go deleted file mode 100644 index 5cec47716..000000000 --- a/_integrations/nrnats/example_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrnats - -import ( - "fmt" - "time" - - "github.com/nats-io/nats.go" - "github.com/nats-io/stan.go" - newrelic "github.com/newrelic/go-agent" -) - -func currentTransaction() newrelic.Transaction { return nil } - -func ExampleStartPublishSegment() { - nc, _ := nats.Connect(nats.DefaultURL) - txn := currentTransaction() - subject := "testing.subject" - - // Start the Publish segment - seg := 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 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)) -} - -var clusterID, clientID string - -// 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. -func ExampleStartPublishSegment_stan() { - sc, _ := stan.Connect(clusterID, clientID) - txn := currentTransaction() - subject := "testing.subject" - - defer StartPublishSegment(txn, sc.NatsConn(), subject).End() - sc.Publish(subject, []byte("Hello World")) -} diff --git a/_integrations/nrnats/examples/README.md b/_integrations/nrnats/examples/README.md deleted file mode 100644 index dfb9d8cdf..000000000 --- a/_integrations/nrnats/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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` - \ No newline at end of file diff --git a/_integrations/nrnats/examples/main.go b/_integrations/nrnats/examples/main.go deleted file mode 100644 index 24542dd09..000000000 --- a/_integrations/nrnats/examples/main.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/nats-io/nats.go" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrnats" -) - -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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - // Initialize agent - cfg := newrelic.NewConfig("NATS App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - var err error - app, err = newrelic.NewApplication(cfg) - 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", nil, nil) - 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) -} diff --git a/_integrations/nrnats/nrnats.go b/_integrations/nrnats/nrnats.go deleted file mode 100644 index 1fae1ff8b..000000000 --- a/_integrations/nrnats/nrnats.go +++ /dev/null @@ -1,66 +0,0 @@ -// 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" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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: newrelic.StartSegmentNow(txn), - 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(), nil, nil) - defer txn.End() - - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageRoutingKey, msg.Sub.Subject, nil) - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageQueueName, msg.Sub.Queue, nil) - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageReplyTo, msg.Reply, nil) - - f(msg) - } -} diff --git a/_integrations/nrnats/nrnats_doc.go b/_integrations/nrnats/nrnats_doc.go deleted file mode 100644 index 71b457de5..000000000 --- a/_integrations/nrnats/nrnats_doc.go +++ /dev/null @@ -1,55 +0,0 @@ -// 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")) -// -// -// 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/_integrations/nrnats/examples/main.go -package nrnats - -import "github.com/newrelic/go-agent/internal" - -func init() { internal.TrackUsage("integration", "framework", "nats") } diff --git a/_integrations/nrnats/nrnats_test.go b/_integrations/nrnats/nrnats_test.go deleted file mode 100644 index d7fa71c36..000000000 --- a/_integrations/nrnats/nrnats_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// 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" - "github.com/nats-io/nats.go" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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, cfgFn) -} - -var cfgFn = func(cfg *newrelic.Config) { - cfg.Enabled = false - cfg.DistributedTracer.Enabled = true - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - 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", nil, nil) - 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", nil, nil) - 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": "OtherTransaction/Go/testing", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "category": "generic", - "name": "MessageBroker/NATS/Topic/Produce/Named/mysubject", - "parentId": internal.MatchAnything, - }, - 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, 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", nil, nil) - 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() - } -} diff --git a/_integrations/nrpkgerrors/README.md b/_integrations/nrpkgerrors/README.md deleted file mode 100644 index 68f3c3cf5..000000000 --- a/_integrations/nrpkgerrors/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrpkgerrors [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpkgerrors?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpkgerrors) - -Package `nrpkgerrors` introduces support for https://github.com/pkg/errors. - -```go -import "github.com/newrelic/go-agent/_integrations/nrpkgerrors" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpkgerrors). diff --git a/_integrations/nrpkgerrors/example/main.go b/_integrations/nrpkgerrors/example/main.go deleted file mode 100644 index 279eb7b36..000000000 --- a/_integrations/nrpkgerrors/example/main.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrpkgerrors" - "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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("pkg/errors app", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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", nil, nil) - e := gamma() - txn.NoticeError(nrpkgerrors.Wrap(e)) - txn.End() - - app.Shutdown(10 * time.Second) -} diff --git a/_integrations/nrpkgerrors/example_test.go b/_integrations/nrpkgerrors/example_test.go deleted file mode 100644 index f29d00aa0..000000000 --- a/_integrations/nrpkgerrors/example_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrpkgerrors_test - -import ( - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrpkgerrors" - "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)) -} diff --git a/_integrations/nrpkgerrors/nrkpgerrors_test.go b/_integrations/nrpkgerrors/nrkpgerrors_test.go deleted file mode 100644 index d4b71faeb..000000000 --- a/_integrations/nrpkgerrors/nrkpgerrors_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// 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" - "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 TestWrappedStackTrace(t *testing.T) { - testcases := []struct { - Error error - ExpectTopFrame string - }{ - {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: ""}, - } - - for idx, tc := range testcases { - e := Wrap(tc.Error) - st := e.(newrelic.StackTracer).StackTrace() - fn := topFrameFunction(st) - if !strings.Contains(fn, tc.ExpectTopFrame) { - t.Errorf("testcase %d: expected %s got %s", - idx, tc.ExpectTopFrame, fn) - } - } -} - -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"}, - } - - for idx, tc := range testcases { - e := Wrap(tc.Error) - class := e.(newrelic.ErrorClasser).ErrorClass() - if class != tc.ExpectClass { - t.Errorf("testcase %d: expected %s got %s", - idx, tc.ExpectClass, class) - } - } -} diff --git a/_integrations/nrpkgerrors/nrpkgerrors.go b/_integrations/nrpkgerrors/nrpkgerrors.go deleted file mode 100644 index f442d4ac9..000000000 --- a/_integrations/nrpkgerrors/nrpkgerrors.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/pkg/errors" -) - -func init() { internal.TrackUsage("integration", "pkg-errors") } - -type nrpkgerror struct { - error -} - -// 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 (e nrpkgerror) StackTrace() []uintptr { - st := deepestStackTrace(e.error) - if nil == st { - return nil - } - return transformStackTrace(st) -} - -func (e nrpkgerror) ErrorClass() string { - if ec, ok := e.error.(newrelic.ErrorClasser); ok { - return ec.ErrorClass() - } - cause := errors.Cause(e.error) - if ec, ok := cause.(newrelic.ErrorClasser); ok { - return ec.ErrorClass() - } - return fmt.Sprintf("%T", cause) -} - -// 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 { - return nrpkgerror{e} -} diff --git a/_integrations/nrpq/README.md b/_integrations/nrpq/README.md deleted file mode 100644 index 1ab62055a..000000000 --- a/_integrations/nrpq/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrpq [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpq?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpq) - -Package `nrpq` instruments https://github.com/lib/pq. - -```go -import "github.com/newrelic/go-agent/_integrations/nrpq" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrpq). diff --git a/_integrations/nrpq/example/main.go b/_integrations/nrpq/example/main.go deleted file mode 100644 index 78da91f2f..000000000 --- a/_integrations/nrpq/example/main.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "database/sql" - "fmt" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - _ "github.com/newrelic/go-agent/_integrations/nrpq" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -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) - } - - cfg := newrelic.NewConfig("PostgreSQL App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - panic(err) - } - app.WaitForConnection(5 * time.Second) - txn := app.StartTransaction("postgresQuery", nil, nil) - - 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) -} diff --git a/_integrations/nrpq/example/sqlx/main.go b/_integrations/nrpq/example/sqlx/main.go deleted file mode 100644 index bf4235af1..000000000 --- a/_integrations/nrpq/example/sqlx/main.go +++ /dev/null @@ -1,146 +0,0 @@ -// 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". -// -// 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/_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/_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" - "fmt" - "log" - "os" - "time" - - "github.com/jmoiron/sqlx" - newrelic "github.com/newrelic/go-agent" - _ "github.com/newrelic/go-agent/_integrations/nrpq" -) - -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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func createApp() newrelic.Application { - cfg := newrelic.NewConfig("SQLx", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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", nil, nil) - 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") -} diff --git a/_integrations/nrpq/nrpq.go b/_integrations/nrpq/nrpq.go deleted file mode 100644 index 0f490ec6a..000000000 --- a/_integrations/nrpq/nrpq.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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/_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/_integrations/nrpq/example/main.go -package nrpq - -import ( - "database/sql" - "database/sql/driver" - "os" - "path" - "regexp" - "strings" - - "github.com/lib/pq" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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 - } -} diff --git a/_integrations/nrpq/nrpq_test.go b/_integrations/nrpq/nrpq_test.go deleted file mode 100644 index 04c8418e7..000000000 --- a/_integrations/nrpq/nrpq_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrpq - -import ( - "testing" - - newrelic "github.com/newrelic/go-agent" -) - -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") - } -} diff --git a/_integrations/nrsqlite3/README.md b/_integrations/nrsqlite3/README.md deleted file mode 100644 index 0bde474fd..000000000 --- a/_integrations/nrsqlite3/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrsqlite3 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrsqlite3?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrsqlite3) - -Package `nrsqlite3` instruments https://github.com/mattn/go-sqlite3. - -```go -import "github.com/newrelic/go-agent/_integrations/nrsqlite3" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrsqlite3). diff --git a/_integrations/nrsqlite3/example/main.go b/_integrations/nrsqlite3/example/main.go deleted file mode 100644 index 72d8d82d5..000000000 --- a/_integrations/nrsqlite3/example/main.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "database/sql" - "fmt" - "os" - "time" - - newrelic "github.com/newrelic/go-agent" - _ "github.com/newrelic/go-agent/_integrations/nrsqlite3" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -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)") - - cfg := newrelic.NewConfig("SQLite App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - if nil != err { - panic(err) - } - app.WaitForConnection(5 * time.Second) - txn := app.StartTransaction("sqliteQuery", nil, nil) - - ctx := newrelic.NewContext(context.Background(), txn) - row := db.QueryRowContext(ctx, "SELECT count(*) from zaps") - var count int - row.Scan(&count) - - txn.End() - app.Shutdown(5 * time.Second) - - fmt.Println("number of entries in table", count) -} diff --git a/_integrations/nrsqlite3/nrsqlite3.go b/_integrations/nrsqlite3/nrsqlite3.go deleted file mode 100644 index 575ae4e22..000000000 --- a/_integrations/nrsqlite3/nrsqlite3.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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/_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/_integrations/nrsqlite3/example/main.go -package nrsqlite3 - -import ( - "database/sql" - "database/sql/driver" - "path/filepath" - "strings" - - sqlite3 "github.com/mattn/go-sqlite3" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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 = "" -} diff --git a/_integrations/nrsqlite3/nrsqlite3_test.go b/_integrations/nrsqlite3/nrsqlite3_test.go deleted file mode 100644 index 6b1b00fd8..000000000 --- a/_integrations/nrsqlite3/nrsqlite3_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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) - } - } -} diff --git a/_integrations/nrstan/README.md b/_integrations/nrstan/README.md deleted file mode 100644 index 5886b8d43..000000000 --- a/_integrations/nrstan/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrstan [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan) - -Package `nrstan` instruments https://github.com/nats-io/stan.go. - -```go -import "github.com/newrelic/go-agent/_integrations/nrstan" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrstan). diff --git a/_integrations/nrstan/examples/README.md b/_integrations/nrstan/examples/README.md deleted file mode 100644 index 186846324..000000000 --- a/_integrations/nrstan/examples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# 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` \ No newline at end of file diff --git a/_integrations/nrstan/examples/main.go b/_integrations/nrstan/examples/main.go deleted file mode 100644 index 5949571d4..000000000 --- a/_integrations/nrstan/examples/main.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/nats-io/stan.go" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrnats" - "github.com/newrelic/go-agent/_integrations/nrstan" -) - -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 mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - // Initialize agent - cfg := newrelic.NewConfig("STAN App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - var err error - app, err = newrelic.NewApplication(cfg) - 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", nil, nil) - 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) -} diff --git a/_integrations/nrstan/nrstan.go b/_integrations/nrstan/nrstan.go deleted file mode 100644 index 17e1fc2ca..000000000 --- a/_integrations/nrstan/nrstan.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrstan - -import ( - stan "github.com/nats-io/stan.go" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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(), nil, nil) - defer txn.End() - - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageRoutingKey, msg.MsgProto.Subject, nil) - integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageReplyTo, msg.MsgProto.Reply, nil) - - f(msg) - } -} diff --git a/_integrations/nrstan/nrstan_doc.go b/_integrations/nrstan/nrstan_doc.go deleted file mode 100644 index c892bd8ab..000000000 --- a/_integrations/nrstan/nrstan_doc.go +++ /dev/null @@ -1,48 +0,0 @@ -// 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/_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/_integrations/nrstan/examples/main.go -package nrstan - -import "github.com/newrelic/go-agent/internal" - -func init() { internal.TrackUsage("integration", "framework", "stan") } diff --git a/_integrations/nrstan/nrstan_test.go b/_integrations/nrstan/nrstan_test.go deleted file mode 100644 index 6070b0e75..000000000 --- a/_integrations/nrstan/nrstan_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrstan - -import ( - "os" - "sync" - "testing" - - "github.com/nats-io/nats-streaming-server/server" - "github.com/nats-io/stan.go" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/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, cfgFn) -} - -var cfgFn = func(cfg *newrelic.Config) { - cfg.Enabled = false - cfg.DistributedTracer.Enabled = true - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - 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, 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, StreamingSubWrapper(app, 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() - } -} diff --git a/_integrations/nrzap/README.md b/_integrations/nrzap/README.md deleted file mode 100644 index 0278aafce..000000000 --- a/_integrations/nrzap/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# _integrations/nrzap [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrzap?status.svg)](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrzap) - -Package `nrzap` supports https://github.com/uber-go/zap. - -```go -import "github.com/newrelic/go-agent/_integrations/nrzap" -``` - -For more information, see -[godocs](https://godoc.org/github.com/newrelic/go-agent/_integrations/nrzap). diff --git a/_integrations/nrzap/example_test.go b/_integrations/nrzap/example_test.go deleted file mode 100644 index 42003b0d5..000000000 --- a/_integrations/nrzap/example_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package nrzap - -import ( - newrelic "github.com/newrelic/go-agent" - "go.uber.org/zap" -) - -func Example() { - cfg := newrelic.NewConfig("Example App", "__YOUR_NEWRELIC_LICENSE_KEY__") - - // Create a new zap logger: - z, _ := zap.NewProduction() - - // Use nrzap to register the logger with the agent: - cfg.Logger = Transform(z.Named("newrelic")) - - newrelic.NewApplication(cfg) -} diff --git a/_integrations/nrzap/nrzap.go b/_integrations/nrzap/nrzap.go deleted file mode 100644 index d18db6f11..000000000 --- a/_integrations/nrzap/nrzap.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 ( - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" - "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} } diff --git a/app_run.go b/app_run.go deleted file mode 100644 index ae0fb00dd..000000000 --- a/app_run.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "encoding/json" - "strings" - "time" - - "github.com/newrelic/go-agent/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 *internal.AttributeConfig - Config Config - - // firstAppName is the value of Config.AppName up to the first semicolon. - firstAppName string -} - -func newAppRun(config Config, reply *internal.ConnectReply) *appRun { - convertConfig := func(c AttributeDestinationConfig) internal.AttributeDestinationConfig { - return internal.AttributeDestinationConfig{ - Enabled: c.Enabled, - Include: c.Include, - Exclude: c.Exclude, - } - } - run := &appRun{ - Reply: reply, - AttributeConfig: internal.CreateAttributeConfig(internal.AttributeConfigInput{ - Attributes: convertConfig(config.Attributes), - ErrorCollector: convertConfig(config.ErrorCollector.Attributes), - TransactionEvents: convertConfig(config.TransactionEvents.Attributes), - TransactionTracer: convertConfig(config.TransactionTracer.Attributes), - BrowserMonitoring: convertConfig(config.BrowserMonitoring.Attributes), - SpanEvents: convertConfig(config.SpanEvents.Attributes), - TraceSegments: convertConfig(config.TransactionTracer.Segments.Attributes), - }, reply.SecurityPolicies.AttributesInclude.Enabled()), - Config: config, - } - - // 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; nil != v { - run.Config.TransactionTracer.Enabled = *v - } - if v := run.Reply.ServerSideConfig.ErrorCollectorEnabled; nil != v { - run.Config.ErrorCollector.Enabled = *v - } - if v := run.Reply.ServerSideConfig.CrossApplicationTracerEnabled; nil != v { - run.Config.CrossApplicationTracer.Enabled = *v - } - if v := run.Reply.ServerSideConfig.TransactionTracerThreshold; nil != v { - 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; nil != v { - run.Config.TransactionTracer.StackTraceThreshold = internal.FloatSecondsToDuration(*v) - } - if v := run.Reply.ServerSideConfig.ErrorCollectorIgnoreStatusCodes; nil != v { - run.Config.ErrorCollector.IgnoreStatusCodes = v - } - - 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] - - if "" != run.Reply.RunID { - js, _ := json.Marshal(settings(run.Config)) - run.Config.Logger.Debug("final configuration", map[string]interface{}{ - "config": internal.JSONString(js), - }) - } - - return run -} - -const ( - // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md#distributed-tracing - serverlessDefaultPrimaryAppID = "Unknown" -) - -const ( - // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md#adaptive-sampling - serverlessSamplerPeriod = 60 * time.Second - serverlessSamplerTarget = 10 -) - -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 - } - - reply.AdaptiveSampler = internal.NewAdaptiveSampler(serverlessSamplerPeriod, - serverlessSamplerTarget, time.Now()) - - 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 - } - for _, ignoreCode := range run.Config.ErrorCollector.IgnoreStatusCodes { - if code == ignoreCode { - return false - } - } - return true -} - -func (run *appRun) txnTraceThreshold(apdexThreshold time.Duration) time.Duration { - if run.Config.TransactionTracer.Threshold.IsApdexFailing { - return internal.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) ptrErrorEvents() *uint { return run.Reply.EventData.Limits.ErrorEvents } -func (run *appRun) ptrSpanEvents() *uint { return run.Reply.EventData.Limits.SpanEvents } - -func (run *appRun) MaxTxnEvents() int { return run.limit(run.Config.MaxTxnEvents(), run.ptrTxnEvents) } -func (run *appRun) MaxCustomEvents() int { - return run.limit(internal.MaxCustomEvents, run.ptrCustomEvents) -} -func (run *appRun) MaxErrorEvents() int { - return run.limit(internal.MaxErrorEvents, run.ptrErrorEvents) -} -func (run *appRun) MaxSpanEvents() int { return run.limit(internal.MaxSpanEvents, run.ptrSpanEvents) } - -func (run *appRun) limit(dflt int, field func() *uint) int { - if nil != field() { - return int(*field()) - } - return dflt -} - -func (run *appRun) ReportPeriods() map[internal.HarvestTypes]time.Duration { - fixed := internal.HarvestMetricsTraces - configurable := internal.HarvestTypes(0) - - for tp, fn := range map[internal.HarvestTypes]func() *uint{ - internal.HarvestTxnEvents: run.ptrTxnEvents, - internal.HarvestCustomEvents: run.ptrCustomEvents, - internal.HarvestErrorEvents: run.ptrErrorEvents, - internal.HarvestSpanEvents: run.ptrSpanEvents, - } { - if nil != run && fn() != nil { - configurable |= tp - } else { - fixed |= tp - } - } - return map[internal.HarvestTypes]time.Duration{ - configurable: run.Reply.ConfigurablePeriod(), - fixed: internal.FixedHarvestPeriod, - } -} diff --git a/app_run_test.go b/app_run_test.go deleted file mode 100644 index 986bf2303..000000000 --- a/app_run_test.go +++ /dev/null @@ -1,388 +0,0 @@ -// 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/internal" -) - -func TestResponseCodeIsError(t *testing.T) { - cfg := NewConfig("my app", "0123456789012345678901234567890123456789") - 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 TestCrossAppTracingEnabled(t *testing.T) { - // CAT should be enabled by default. - cfg := NewConfig("my app", "0123456789012345678901234567890123456789") - run := newAppRun(cfg, internal.ConnectReplyDefaults()) - if enabled := run.Config.CrossApplicationTracer.Enabled; !enabled { - t.Error(enabled) - } - - // DT gets priority over CAT. - cfg = NewConfig("my app", "0123456789012345678901234567890123456789") - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = true - run = newAppRun(cfg, internal.ConnectReplyDefaults()) - if enabled := run.Config.CrossApplicationTracer.Enabled; enabled { - t.Error(enabled) - } - - cfg = NewConfig("my app", "0123456789012345678901234567890123456789") - cfg.DistributedTracer.Enabled = false - cfg.CrossApplicationTracer.Enabled = false - run = newAppRun(cfg, internal.ConnectReplyDefaults()) - if enabled := run.Config.CrossApplicationTracer.Enabled; enabled { - t.Error(enabled) - } - - cfg = NewConfig("my app", "0123456789012345678901234567890123456789") - 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 := NewConfig("my app", "0123456789012345678901234567890123456789") - 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 = NewConfig("my app", "0123456789012345678901234567890123456789") - 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 = NewConfig("my app", "0123456789012345678901234567890123456789") - 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 = NewConfig("my app", "0123456789012345678901234567890123456789") - 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) - } -} - -var cfg = NewConfig("name", "license") - -func TestEmptyReplyEventHarvestDefaults(t *testing.T) { - var run internal.HarvestConfigurer = newAppRun(cfg, &internal.ConnectReply{}) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: internal.MaxTxnEvents, - maxCustomEvents: internal.MaxCustomEvents, - maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: internal.MaxSpanEvents, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll: 60 * time.Second, - 0: 60 * time.Second, - }, - }) -} - -func TestEventHarvestFieldsAllPopulated(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 5000, - "harvest_limits": { - "analytic_event_data": 1, - "custom_event_data": 2, - "span_event_data": 3, - "error_event_data": 4 - } - } - }}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: 1, - maxCustomEvents: 2, - maxErrorEvents: 4, - maxSpanEvents: 3, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestMetricsTraces: 60 * time.Second, - internal.HarvestTypesEvents: 5 * time.Second, - }, - }) -} - -func TestZeroReportPeriod(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 0 - } - }}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: internal.MaxTxnEvents, - maxCustomEvents: internal.MaxCustomEvents, - maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: internal.MaxSpanEvents, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll: 60 * time.Second, - 0: 60 * time.Second, - }, - }) -} - -func TestEventHarvestFieldsOnlySpanEvents(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 5000, - "harvest_limits": { "span_event_data": 3 } - }}}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: internal.MaxTxnEvents, - maxCustomEvents: internal.MaxCustomEvents, - maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: 3, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll ^ internal.HarvestSpanEvents: 60 * time.Second, - internal.HarvestSpanEvents: 5 * time.Second, - }, - }) -} - -func TestEventHarvestFieldsOnlyTxnEvents(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 5000, - "harvest_limits": { "analytic_event_data": 3 } - }}}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: 3, - maxCustomEvents: internal.MaxCustomEvents, - maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: internal.MaxSpanEvents, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll ^ internal.HarvestTxnEvents: 60 * time.Second, - internal.HarvestTxnEvents: 5 * time.Second, - }, - }) -} - -func TestEventHarvestFieldsOnlyErrorEvents(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 5000, - "harvest_limits": { "error_event_data": 3 } - }}}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: internal.MaxTxnEvents, - maxCustomEvents: internal.MaxCustomEvents, - maxErrorEvents: 3, - maxSpanEvents: internal.MaxSpanEvents, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll ^ internal.HarvestErrorEvents: 60 * time.Second, - internal.HarvestErrorEvents: 5 * time.Second, - }, - }) -} - -func TestEventHarvestFieldsOnlyCustomEvents(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "report_period_ms": 5000, - "harvest_limits": { "custom_event_data": 3 } - }}}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - var run internal.HarvestConfigurer = newAppRun(cfg, reply) - assertHarvestConfig(t, &run, expectHarvestConfig{ - maxTxnEvents: internal.MaxTxnEvents, - maxCustomEvents: 3, - maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: internal.MaxSpanEvents, - periods: map[internal.HarvestTypes]time.Duration{ - internal.HarvestTypesAll ^ internal.HarvestCustomEvents: 60 * time.Second, - internal.HarvestCustomEvents: 5 * time.Second, - }, - }) -} - -func TestConfigurableHarvestNegativeReportPeriod(t *testing.T) { - h, err := internal.ConstructConnectReply([]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) != 16 || len(id2) != 16 || id1 == id2 { - t.Error(id1, id2) - } -} - -func TestConfigurableTxnEvents_withCollResponse(t *testing.T) { - h, err := internal.ConstructConnectReply([]byte( - `{"return_value":{ - "event_harvest_config": { - "report_period_ms": 10000, - "harvest_limits": { - "analytic_event_data": 15 - } - } - }}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - result := newAppRun(cfg, h).MaxTxnEvents() - if result != 15 { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", 15, result)) - } -} - -func TestConfigurableTxnEvents_notInCollResponse(t *testing.T) { - reply, err := internal.ConstructConnectReply([]byte( - `{"return_value":{ - "event_harvest_config": { - "report_period_ms": 10000 - } - }}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - expected := 10 - cfg.TransactionEvents.MaxSamplesStored = expected - result := newAppRun(cfg, reply).MaxTxnEvents() - if result != expected { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", expected, result)) - } -} - -func TestConfigurableTxnEvents_configMoreThanMax(t *testing.T) { - h, err := internal.ConstructConnectReply([]byte( - `{"return_value":{ - "event_harvest_config": { - "report_period_ms": 10000 - } - }}`), internal.PreconnectReply{}) - if nil != err { - t.Fatal(err) - } - cfg.TransactionEvents.MaxSamplesStored = internal.MaxTxnEvents + 100 - result := newAppRun(cfg, h).MaxTxnEvents() - if result != internal.MaxTxnEvents { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", internal.MaxTxnEvents, result)) - } -} - -type expectHarvestConfig struct { - maxTxnEvents int - maxCustomEvents int - maxErrorEvents int - maxSpanEvents int - periods map[internal.HarvestTypes]time.Duration -} - -func assertHarvestConfig(t testing.TB, hc *internal.HarvestConfigurer, expect expectHarvestConfig) { - if h, ok := t.(interface { - Helper() - }); ok { - h.Helper() - } - if max := (*hc).MaxTxnEvents(); max != expect.maxTxnEvents { - t.Error(max, expect.maxTxnEvents) - } - if max := (*hc).MaxCustomEvents(); max != expect.maxCustomEvents { - t.Error(max, expect.maxCustomEvents) - } - if max := (*hc).MaxSpanEvents(); max != expect.maxSpanEvents { - t.Error(max, expect.maxSpanEvents) - } - if max := (*hc).MaxErrorEvents(); max != expect.maxErrorEvents { - t.Error(max, expect.maxErrorEvents) - } - if periods := (*hc).ReportPeriods(); !reflect.DeepEqual(periods, expect.periods) { - t.Error(periods, expect.periods) - } -} diff --git a/application.go b/application.go deleted file mode 100644 index f1b4ea264..000000000 --- a/application.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "net/http" - "time" -) - -// Application represents your application. -type Application interface { - // StartTransaction begins a Transaction. - // * Transaction.NewGoroutine() must be used to pass the Transaction - // between goroutines. - // * This method never returns nil. - // * The Transaction is considered a web transaction if an http.Request - // is provided. - // * The transaction returned implements the http.ResponseWriter - // interface. Provide your ResponseWriter as a parameter and - // then use the Transaction in its place to instrument the response - // code and response headers. - StartTransaction(name string, w http.ResponseWriter, r *http.Request) Transaction - - // 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 returned if event type or params is invalid. - RecordCustomEvent(eventType string, params map[string]interface{}) error - - // RecordCustomMetric records a custom metric. The metric name you - // provide will be prefixed by "Custom/". Custom metrics are not - // currently supported in serverless mode. - // - // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/collect-custom-metrics - RecordCustomMetric(name string, value float64) 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. - WaitForConnection(timeout time.Duration) error - - // 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. - Shutdown(timeout time.Duration) -} - -// 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. Applications do not share global state, therefore it is safe -// to create multiple applications. -func NewApplication(c Config) (Application, error) { - return newApp(c) -} diff --git a/attributes.go b/attributes.go deleted file mode 100644 index f4966d195..000000000 --- a/attributes.go +++ /dev/null @@ -1,119 +0,0 @@ -// 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, -// and traced errors. 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 = "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" -) - -// Attributes destined for Errors and Transaction Traces: -const ( - // AttributeRequestUserAgent is the request's "User-Agent" header. - AttributeRequestUserAgent = "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 _integrations -// (https://godoc.org/github.com/newrelic/go-agent/_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, -// AttributeMessageExchangeType, AttributeMessageReplyTo, -// 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#Transaction) API. In this -// case, these attributes will be included on all destintations by default. -// -// txn := app.StartTransaction("Message/RabbitMQ/Exchange/Named/MyExchange", nil, nil) -// txn.AddAttribute(AttributeMessageRoutingKey, "myRoutingKey") -// txn.AddAttribute(AttributeMessageQueueName, "myQueueName") -// txn.AddAttribute(AttributeMessageExchangeType, "myExchangeType") -// txn.AddAttribute(AttributeMessageReplyTo, "myReplyTo") -// txn.AddAttribute(AttributeMessageCorrelationID, "myCorrelationID") -// // ... consume a message ... -// txn.End() -// -// It is recommended that at most one message is consumed per transaction. -const ( - // 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" -) - -// Attributes destined for Span Events: -// -// 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" - SpanAttributeAWSRequestID = "aws.requestId" - SpanAttributeAWSRegion = "aws.region" -) diff --git a/browser_header.go b/browser_header.go deleted file mode 100644 index c20c86b56..000000000 --- a/browser_header.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "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) -} diff --git a/browser_header_test.go b/browser_header_test.go deleted file mode 100644 index 4647ea8d9..000000000 --- a/browser_header_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "fmt" - "testing" - - "github.com/newrelic/go-agent/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)) - } -} diff --git a/build-script.sh b/build-script.sh deleted file mode 100755 index 80db9a5ab..000000000 --- a/build-script.sh +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2020 New Relic Corporation. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -x -set -e - -LATEST_VERSION="go1.15" - -# NOTE: Once we get rid of travis for good, this whole section can be removed -# along with the .travis.yml file. -if [[ -n "$(go version | grep $LATEST_VERSION)" ]] && [[ "$TRAVIS" == "true" ]]; then - echo "Installing updated glibc\n" - # can we get this from an actual repository? - curl -LO 'http://launchpadlibrarian.net/130794928/libc6_2.17-0ubuntu4_amd64.deb' - sudo dpkg -i libc6_2.17-0ubuntu4_amd64.deb -else - echo "Skipping glibc update\n" -fi - -pwd=$(pwd) - -# inputs -# 1: repo pin; example: github.com/rewrelic/go-agent@v1.9.0 -pin_go_dependency() { - if [[ ! -z "$1" ]]; then - echo "Pinning: $1" - repo=$(echo "$1" | cut -d '@' -f1) - pinTo=$(echo "$1" | cut -d '@' -f2) - set +e - go get -u "$repo" # this go get will fail to build - set -e - cd "$GOPATH"/src/"$repo" - git checkout "$pinTo" - cd - - fi -} - -go_mod_tidy() { - go mod tidy -} - -IFS="," -for dir in $DIRS; do - cd "$pwd/$dir" - - if [ -f "go.mod" ]; then - go mod edit -replace github.com/newrelic/go-agent/v3="$pwd"/v3 - fi - - # Do tidy if we can - go_mod_tidy || true - - pin_go_dependency "$PIN" - - # avoid testing v3 code when testing v2 newrelic package - if [ "$dir" == "." ]; then - rm -rf v3/ - else - # Only v3 code version 1.9+ needs GRPC dependencies - VERSION=$(go version) - V1_7="1.7" - V1_8="1.8" - V1_9="1.9" - V1_10="1.10" - V1_11="1.11" - V1_12="1.12" - V1_13="1.13" - V1_14="1.14" - if [[ "$VERSION" =~ .*"$V1_7".* || "$VERSION" =~ .*"$V1_8".* ]]; then - echo "Not installing GRPC for old versions" - elif [[ "$VERSION" =~ .*"$V1_9" || "$VERSION" =~ .*"$V1_10" || "$VERSION" =~ .*"$V1_11" || "$VERSION" =~ .*"$V1_12" || "$VERSION" =~ .*"$V1_13" || "$VERSION" =~ .*"$V1_14" ]]; then - # install v3 dependencies that support this go version - pin_go_dependency "google.golang.org/grpc@v1.31.0" - pin_go_dependency "golang.org/x/net/http2@7fd8e65b642006927f6cec5cb4241df7f98a2210" - - # install protobuf once dependencies are resolved - go get -u github.com/golang/protobuf/protoc-gen-go - else - go get -u github.com/golang/protobuf/protoc-gen-go - go get -u google.golang.org/grpc - fi - fi - - # go get is necessary for testing v2 integrations since they do not have - # a go.mod file. - if [[ $dir =~ "_integrations" ]]; then - go get -t ./... - fi - - go test -race -benchtime=1ms -bench=. ./... - go vet ./... - - # Test again against the latest version of the dependencies to ensure that - # our instrumentation is up to date. TODO: Perhaps it is possible to - # upgrade all go.mod dependencies to latest master with a go command. - if [ -n "$EXTRATESTING" ]; then - eval "$EXTRATESTING" - go test -race -benchtime=1ms -bench=. ./... - fi - - if [[ -n "$(go version | grep $LATEST_VERSION)" ]]; then - # golint requires a supported version of Go, which in practice is currently 1.9+. - # See: https://github.com/golang/lint#installation - # For simplicity, run it on a single Go version. - go get -u golang.org/x/lint/golint - # do not expect golint to be in the PATH, instead use go list to discover - # the path to the binary. - $(go list -f {{.Target}} golang.org/x/lint/golint) -set_exit_status ./... - - # only run gofmt on a single version as the format changed from 1.10 to - # 1.11. - if [ -n "$(gofmt -s -l .)" ]; then - exit 1 - fi - fi -done diff --git a/config.go b/config.go deleted file mode 100644 index 85caca8fa..000000000 --- a/config.go +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/newrelic/go-agent/internal" -) - -// Config contains Application and Transaction behavior settings. -// Use NewConfig to create a Config with proper defaults. -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. For info level logging to stdout: - // - // cfg.Logger = newrelic.NewLogger(os.Stdout) - // - // For debug level logging to stdout: - // - // cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - // - // 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 { - // Enabled controls whether RecordCustomEvent will collect - // custom analytics events. High security mode overrides this - // setting. - Enabled bool - } - - // 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, with the exception of 404, are - // turned into errors. - IgnoreStatusCodes []int - // Attributes controls the attributes included with errors. - Attributes AttributeDestinationConfig - } - - // 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 - } - // SegmentThreshold 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. - SegmentThreshold time.Duration - // 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 - // Attributes controls the attributes included with transaction - // traces. - Attributes AttributeDestinationConfig - // Segments.Attributes controls the attributes included with - // each trace segment. - Segments struct { - 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 - } - - // CrossApplicationTracer controls behaviour relating to cross application - // tracing (CAT), available since Go Agent v0.11. The - // CrossApplicationTracer and the DistributedTracer cannot be - // simultaneously enabled. - // - // https://docs.newrelic.com/docs/apm/transactions/cross-application-traces/introduction-cross-application-traces - CrossApplicationTracer struct { - Enabled bool - } - - // DistributedTracer controls behaviour relating to Distributed Tracing, - // available since Go Agent v2.1. The DistributedTracer and the - // CrossApplicationTracer cannot be simultaneously enabled. - // - // https://docs.newrelic.com/docs/apm/distributed-tracing/getting-started/introduction-distributed-tracing - DistributedTracer struct { - Enabled bool - } - - // SpanEvents controls behavior relating to Span Events. Span Events - // require that DistributedTracer is enabled. - SpanEvents struct { - Enabled bool - Attributes AttributeDestinationConfig - } - - // 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 - } - // 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 - } - } - - // 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 - } -} - -// 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 -} - -// NewConfig creates a Config populated with default settings and the given -// appname and license. -func NewConfig(appname, license string) Config { - c := Config{} - - c.AppName = appname - c.License = license - c.Enabled = true - c.Labels = make(map[string]string) - c.CustomInsightsEvents.Enabled = true - 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.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.SegmentThreshold = 2 * time.Millisecond - c.TransactionTracer.StackTraceThreshold = 500 * time.Millisecond - c.TransactionTracer.Attributes.Enabled = true - c.TransactionTracer.Segments.Attributes.Enabled = true - - c.BrowserMonitoring.Enabled = true - // browser monitoring attributes are disabled by default - c.BrowserMonitoring.Attributes.Enabled = false - - c.CrossApplicationTracer.Enabled = true - c.DistributedTracer.Enabled = false - c.SpanEvents.Enabled = true - c.SpanEvents.Attributes.Enabled = true - - 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.ServerlessMode.ApdexThreshold = 500 * time.Millisecond - c.ServerlessMode.Enabled = false - - 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") -) - -// 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 - } - return nil -} - -// MaxTxnEvents returns the configured maximum number of Transaction Events if it has been configured -// and is less than the default maximum; otherwise it returns the default max. -func (c Config) MaxTxnEvents() int { - configured := c.TransactionEvents.MaxSamplesStored - if configured < 0 || configured > internal.MaxTxnEvents { - return internal.MaxTxnEvents - } - return configured -} diff --git a/context.go b/context.go deleted file mode 100644 index 159bea7d1..000000000 --- a/context.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package newrelic - -import ( - "context" - "net/http" - - "github.com/newrelic/go-agent/internal" -) - -// NewContext returns a new 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 { - 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 -} diff --git a/context_stub.go b/context_stub.go deleted file mode 100644 index b4e05584f..000000000 --- a/context_stub.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build !go1.7 - -package newrelic - -import "net/http" - -// RequestWithTransactionContext adds the transaction to the request's context. -func RequestWithTransactionContext(req *http.Request, txn Transaction) *http.Request { - return req -} - -func transactionFromRequestContext(req *http.Request) Transaction { - return nil -} diff --git a/datastore.go b/datastore.go deleted file mode 100644 index 65f0fbf26..000000000 --- a/datastore.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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. See -// https://github.com/newrelic/go-agent/blob/master/datastore.go for the full -// list of available DatastoreProducts. -type DatastoreProduct string - -// Datastore names used across New Relic agents: -const ( - DatastoreCassandra DatastoreProduct = "Cassandra" - DatastoreDerby DatastoreProduct = "Derby" - DatastoreElasticsearch DatastoreProduct = "Elasticsearch" - DatastoreFirebird DatastoreProduct = "Firebird" - DatastoreIBMDB2 DatastoreProduct = "IBMDB2" - DatastoreInformix DatastoreProduct = "Informix" - DatastoreMemcached DatastoreProduct = "Memcached" - DatastoreMongoDB DatastoreProduct = "MongoDB" - DatastoreMySQL DatastoreProduct = "MySQL" - DatastoreMSSQL DatastoreProduct = "MSSQL" - DatastoreNeptune DatastoreProduct = "Neptune" - DatastoreOracle DatastoreProduct = "Oracle" - DatastorePostgres DatastoreProduct = "Postgres" - DatastoreRedis DatastoreProduct = "Redis" - DatastoreSolr DatastoreProduct = "Solr" - DatastoreSQLite DatastoreProduct = "SQLite" - DatastoreCouchDB DatastoreProduct = "CouchDB" - DatastoreRiak DatastoreProduct = "Riak" - DatastoreVoltDB DatastoreProduct = "VoltDB" - DatastoreDynamoDB DatastoreProduct = "DynamoDB" - DatastoreAerospike DatastoreProduct = "Aerospike" -) diff --git a/doc.go b/doc.go deleted file mode 100644 index cad90906f..000000000 --- a/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Package newrelic provides instrumentation for Go applications. -// -// Deprecated: This package has been supplanted by version 3 here: -// -// https://godoc.org/github.com/newrelic/go-agent/v3/newrelic -package newrelic diff --git a/errors.go b/errors.go deleted file mode 100644 index 93592f4f2..000000000 --- a/errors.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import "github.com/newrelic/go-agent/internal" - -// 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 that implements ErrorClasser, ErrorAttributer, and -// StackTracer. Use it with Transaction.NoticeError to directly control error -// 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 which can be assigned to the Error -// struct's Stack field or returned by an error that implements the ErrorClasser -// interface. -func NewStackTrace() []uintptr { - st := internal.GetStackTrace() - return []uintptr(st) -} - -func (e Error) Error() string { return e.Message } - -// ErrorClass implements the ErrorClasser interface. -func (e Error) ErrorClass() string { return e.Class } - -// ErrorAttributes implements the ErrorAttributes interface. -func (e Error) ErrorAttributes() map[string]interface{} { return e.Attributes } - -// StackTrace implements the StackTracer interface. -func (e Error) StackTrace() []uintptr { return e.Stack } diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index acb8934fd..000000000 --- a/examples/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The examples in this directory are for the now deprecated v2 New Relic Go -Agent. Examples for the most recent v3 version of the agent can be found in -[v3/examples](../v3/examples). diff --git a/examples/client-round-tripper/main.go b/examples/client-round-tripper/main.go deleted file mode 100644 index 21f2e0630..000000000 --- a/examples/client-round-tripper/main.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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 NewRoundTripper. -package main - -import ( - "fmt" - "net/http" - "os" - "time" - - "github.com/newrelic/go-agent" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func doRequest(txn newrelic.Transaction) error { - for _, addr := range []string{"segments", "mysql"} { - url := fmt.Sprintf("http://localhost:8000/%s", addr) - req, err := http.NewRequest("GET", url, nil) - if nil != err { - return err - } - client := &http.Client{} - - // Using NewRoundTripper automatically instruments all request - // for Distributed Tracing and Cross Application Tracing. - client.Transport = newrelic.NewRoundTripper(txn, nil) - - resp, err := client.Do(req) - if nil != err { - return err - } - fmt.Println("response code is", resp.StatusCode) - } - return nil -} - -func main() { - cfg := newrelic.NewConfig("Client App RoundTripper", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - cfg.DistributedTracer.Enabled = true - app, err := newrelic.NewApplication(cfg) - 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) - } - - txn := app.StartTransaction("client-txn", nil, nil) - err = doRequest(txn) - if nil != err { - txn.NoticeError(err) - } - txn.End() - - // Shut down the application to flush data to New Relic. - app.Shutdown(10 * time.Second) -} diff --git a/examples/client/main.go b/examples/client/main.go deleted file mode 100644 index b655bce80..000000000 --- a/examples/client/main.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "net/http" - "os" - "time" - - "github.com/newrelic/go-agent" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func doRequest(txn newrelic.Transaction) error { - req, err := http.NewRequest("GET", "http://localhost:8000/segments", nil) - if nil != err { - return err - } - client := &http.Client{} - seg := newrelic.StartExternalSegment(txn, req) - defer seg.End() - resp, err := client.Do(req) - if nil != err { - return err - } - fmt.Println("response code is", resp.StatusCode) - return nil -} - -func main() { - cfg := newrelic.NewConfig("Client App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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) - } - - txn := app.StartTransaction("client-txn", nil, nil) - err = doRequest(txn) - if nil != err { - txn.NoticeError(err) - } - txn.End() - - // Shut down the application to flush data to New Relic. - app.Shutdown(10 * time.Second) -} diff --git a/examples/custom-instrumentation/main.go b/examples/custom-instrumentation/main.go deleted file mode 100644 index ed6969b4b..000000000 --- a/examples/custom-instrumentation/main.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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" - "os" - "os/exec" - "time" - - "github.com/newrelic/go-agent" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func called(app newrelic.Application, payload string) { - txn := app.StartTransaction("called-txn", nil, nil) - defer txn.End() - - // Accept the payload that was passed on the command line. - txn.AcceptDistributedTracePayload(newrelic.TransportOther, payload) - time.Sleep(1 * time.Second) -} - -func calling(app newrelic.Application) { - txn := app.StartTransaction("calling-txn", nil, nil) - defer txn.End() - - // Create a payload, start the called process and pass the payload. - payload := txn.CreateDistributedTracePayload() - cmd := exec.Command(os.Args[0], payload.Text()) - 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) { - cfg := newrelic.NewConfig(name, mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - - // Distributed Tracing and Cross Application Tracing cannot both be - // enabled at the same time. - cfg.DistributedTracer.Enabled = true - - app, err := newrelic.NewApplication(cfg) - - 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) -} diff --git a/examples/server-http/main.go b/examples/server-http/main.go deleted file mode 100644 index d4c39ecae..000000000 --- a/examples/server-http/main.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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" -) - -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", writer, req) - defer txn.End() - - if req.URL.String() == "/segments" { - defer newrelic.StartSegment(txn, "f1").End() - - func() { - defer newrelic.StartSegment(txn, "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. - txn.WriteHeader(http.StatusNotFound) - } -} - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func makeApplication() (newrelic.Application, error) { - cfg := newrelic.NewConfig("HTTP Server App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - cfg.DistributedTracer.Enabled = true - app, err := newrelic.NewApplication(cfg) - - 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() -} diff --git a/examples/server/main.go b/examples/server/main.go deleted file mode 100644 index 1bf6a248e..000000000 --- a/examples/server/main.go +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package main - -import ( - "errors" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "os" - "sync" - "time" - - newrelic "github.com/newrelic/go-agent" -) - -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") - - if txn := newrelic.FromContext(r.Context()); txn != nil { - txn.NoticeError(errors.New("my error message")) - } -} - -func noticeErrorWithAttributes(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, "noticing an error") - - if txn := newrelic.FromContext(r.Context()); txn != nil { - 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") - - if nil != txn { - 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") - - if txn := newrelic.FromContext(r.Context()); txn != nil { - txn.SetName("other-name") - } -} - -func addAttribute(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, "adding attributes") - - if txn := newrelic.FromContext(r.Context()); txn != nil { - txn.AddAttribute("myString", "hello") - txn.AddAttribute("myInt", 123) - } -} - -func ignore(w http.ResponseWriter, r *http.Request) { - if coinFlip := (0 == rand.Intn(2)); coinFlip { - if txn := newrelic.FromContext(r.Context()); txn != nil { - 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 newrelic.StartSegment(txn, "f1").End() - - func() { - defer newrelic.StartSegment(txn, "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: newrelic.StartSegmentNow(txn), - // 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: newrelic.StartSegmentNow(txn), - 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", "http://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: newrelic.StartSegmentNow(txn), - // URL: urlString, - // } - // - es := newrelic.StartExternalSegment(txn, req) - resp, err := http.DefaultClient.Do(req) - es.End() - - if nil != err { - 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(nil, client.Transport) - - request, _ := http.NewRequest("GET", "http://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 nil != err { - 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 newrelic.StartSegment(txn, "async").End() - time.Sleep(100 * time.Millisecond) - }(txn.NewGoroutine()) - - segment := newrelic.StartSegment(txn, "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. - if nil != txn { - 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, 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") -} - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Example App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - - app, err := newrelic.NewApplication(cfg) - if nil != err { - 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_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, "/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("/background", func(w http.ResponseWriter, req *http.Request) { - // Transactions started without an http.Request are classified as - // background transactions. - txn := app.StartTransaction("background", nil, nil) - defer txn.End() - - io.WriteString(w, "background transaction") - time.Sleep(150 * time.Millisecond) - }) - - http.ListenAndServe(":8000", nil) -} diff --git a/examples/short-lived-process/main.go b/examples/short-lived-process/main.go deleted file mode 100644 index 7f3fd1ac0..000000000 --- a/examples/short-lived-process/main.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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" -) - -func mustGetEnv(key string) string { - if val := os.Getenv(key); "" != val { - return val - } - panic(fmt.Sprintf("environment variable %s unset", key)) -} - -func main() { - cfg := newrelic.NewConfig("Short Lived App", mustGetEnv("NEW_RELIC_LICENSE_KEY")) - cfg.Logger = newrelic.NewDebugLogger(os.Stdout) - app, err := newrelic.NewApplication(cfg) - 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", nil, nil) - 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) -} diff --git a/examples_test.go b/examples_test.go deleted file mode 100644 index 07e823d26..000000000 --- a/examples_test.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package newrelic - -import ( - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - "time" -) - -func Example() { - // First create a Config. - cfg := NewConfig("Example Application", "__YOUR_NEW_RELIC_LICENSE_KEY__") - - // Modify Config fields to control agent behavior. - cfg.Logger = NewDebugLogger(os.Stdout) - - // Now use the Config the create an Application. - app, err := NewApplication(cfg) - 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", nil, nil) - defer txn.End() - - time.Sleep(time.Second) - }() - - // WrapHandler and WrapHandleFunc make it easy to instrument inbound web - // requests handled by the http standard library without calling - // StartTransaction. Popular framework instrumentation packages exist - // in the _integrations directory. - http.HandleFunc(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 := 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 StartSegment(txn, "helperFunction").End() - }() - - io.WriteString(w, "hello world") - } - http.HandleFunc(WrapHandleFunc(app, "/hello", helloHandler)) - http.ListenAndServe(":8000", nil) -} - -func currentTransaction() Transaction { - return nil -} - -func ExampleNewRoundTripper() { - client := &http.Client{} - // The RoundTripper returned by NewRoundTripper instruments all requests - // done by this client with external segments. - client.Transport = NewRoundTripper(nil, client.Transport) - - request, _ := http.NewRequest("GET", "http://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 = RequestWithTransactionContext(request, txn) - - client.Do(request) -} - -func getApp() 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. - if txn := FromContext(req.Context()); nil != txn { - 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") - } - http.HandleFunc(WrapHandleFunc(getApp(), "/browser", handler)) - http.ListenAndServe(":8000", nil) -} - -func ExampleDatastoreSegment() { - txn := currentTransaction() - ds := &DatastoreSegment{ - StartTime: StartSegmentNow(txn), - // Product, Collection, and Operation are the primary metric - // aggregation fields which we encourage you to populate. - Product: DatastoreMySQL, - Collection: "users_table", - Operation: "SELECT", - } - // your database call here - ds.End() -} - -func ExampleMessageProducerSegment() { - txn := currentTransaction() - seg := &MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationType: 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(Error{ - Message: e.Error(), - Class: "LoginError", - Attributes: map[string]interface{}{ - "username": username, - }, - }) -} - -func ExampleExternalSegment() { - txn := currentTransaction() - client := &http.Client{} - request, _ := http.NewRequest("GET", "http://www.example.com", nil) - segment := 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 := ExternalSegment{ - StartTime: StartSegmentNow(txn), - // 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", "http://www.example.com", nil) - segment := StartExternalSegment(txn, request) - response, _ := client.Do(request) - segment.Response = response - segment.End() -} - -func ExampleStartExternalSegment_context() { - txn := currentTransaction() - request, _ := http.NewRequest("GET", "http://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 = RequestWithTransactionContext(request, txn) - segment := StartExternalSegment(nil, request) - - client := &http.Client{} - response, _ := client.Do(request) - segment.Response = response - segment.End() -} - -func ExampleNewStaticWebRequest() { - app := getApp() - webReq := NewStaticWebRequest(http.Header{}, &url.URL{Path: "path"}, "GET", TransportHTTP) - txn := app.StartTransaction("My-Transaction", nil, nil) - txn.SetWebRequest(webReq) -} diff --git a/instrumentation.go b/instrumentation.go deleted file mode 100644 index bd3edf783..000000000 --- a/instrumentation.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "net/http" -) - -// 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) { -// if txn := newrelic.FromContext(req.Context()); nil != txn { -// txn.AddAttribute("customerLevel", "gold") -// } -// } -// -// This function is safe to call if app is nil. -func WrapHandle(app Application, pattern string, handler http.Handler) (string, http.Handler) { - if app == nil { - return pattern, handler - } - return pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - txn := app.StartTransaction(pattern, w, r) - defer txn.End() - - r = RequestWithTransactionContext(r, txn) - - handler.ServeHTTP(txn, r) - }) -} - -// 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(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(WrapHandleFunc(app, "/users", func(w http.ResponseWriter, req *http.Request) { -// if txn := newrelic.FromContext(req.Context()); nil != txn { -// txn.AddAttribute("customerLevel", "gold") -// } -// io.WriteString(w, "users page") -// })) -// -// This function is safe to call if app is nil. -func WrapHandleFunc(app Application, pattern string, handler func(http.ResponseWriter, *http.Request)) (string, func(http.ResponseWriter, *http.Request)) { - p, h := WrapHandle(app, pattern, http.HandlerFunc(handler)) - return p, func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) } -} - -// NewRoundTripper creates an http.RoundTripper to instrument external requests -// and add distributed tracing headers. The RoundTripper returned creates an -// external segment before delegating to the original RoundTripper provided (or -// http.DefaultTransport if none is provided). If the Transaction parameter is -// nil then the RoundTripper will look for a Transaction in the request's -// context (using FromContext). Using a nil Transaction is STRONGLY recommended -// because it allows the same RoundTripper (and client) to be reused for -// multiple transactions. -func NewRoundTripper(txn Transaction, original http.RoundTripper) http.RoundTripper { - 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(txn, request) - - if nil == original { - original = http.DefaultTransport - } - 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) } diff --git a/internal/adaptive_sampler.go b/internal/adaptive_sampler.go deleted file mode 100644 index 3d45eae4c..000000000 --- a/internal/adaptive_sampler.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "math" - "sync" - "time" -) - -// AdaptiveSampler calculates which transactions should be sampled. An interface -// is used in the connect reply to facilitate testing. -type AdaptiveSampler interface { - ComputeSampled(priority float32, now time.Time) bool -} - -// SampleEverything is used for testing. -type SampleEverything struct{} - -// SampleNothing is used when the application is not yet connected. -type SampleNothing struct{} - -// ComputeSampled implements AdaptiveSampler. -func (s SampleEverything) ComputeSampled(priority float32, now time.Time) bool { return true } - -// ComputeSampled implements AdaptiveSampler. -func (s SampleNothing) ComputeSampled(priority float32, now time.Time) bool { return false } - -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() - - // 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) -} diff --git a/internal/adaptive_sampler_test.go b/internal/adaptive_sampler_test.go deleted file mode 100644 index ae8212991..000000000 --- a/internal/adaptive_sampler_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 TestDefaultReplyValidSampler(t *testing.T) { - reply := ConnectReplyDefaults() - assert(t, !reply.AdaptiveSampler.ComputeSampled(1.0, time.Now())) -} - -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 -} diff --git a/internal/analytics_events.go b/internal/analytics_events.go deleted file mode 100644 index 088d37fa1..000000000 --- a/internal/analytics_events.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "container/heap" - - "github.com/newrelic/go-agent/internal/jsonx" -) - -type analyticsEvent struct { - priority Priority - jsonWriter -} - -type analyticsEventHeap []analyticsEvent - -type analyticsEvents struct { - numSeen int - events analyticsEventHeap - failedHarvests int -} - -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 -} diff --git a/internal/analytics_events_test.go b/internal/analytics_events_test.go deleted file mode 100644 index 876199535..000000000 --- a/internal/analytics_events_test.go +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "strconv" - "testing" - "time" -) - -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(MaxTxnEvents) - event := analyticsEvent{0, w} - for n := 0; n < 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)) - } -} diff --git a/internal/apdex.go b/internal/apdex.go deleted file mode 100644 index e4ad6b152..000000000 --- a/internal/apdex.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 "" - } -} diff --git a/internal/apdex_test.go b/internal/apdex_test.go deleted file mode 100644 index 6a03f23ca..000000000 --- a/internal/apdex_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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) - } -} diff --git a/internal/attributes.go b/internal/attributes.go deleted file mode 100644 index e31fd8d4e..000000000 --- a/internal/attributes.go +++ /dev/null @@ -1,614 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "fmt" - "net/http" - "net/url" - "sort" - "strconv" - "strings" -) - -// AgentAttributeID uniquely identifies each agent attribute. -type AgentAttributeID int - -// New agent attributes must be added in the following places: -// * Constants here. -// * Top level attributes.go file. -// * agentAttributeInfo -const ( - AttributeHostDisplayName AgentAttributeID = iota - attributeRequestMethod - attributeRequestAcceptHeader - attributeRequestContentType - attributeRequestContentLength - attributeRequestHeadersHost - attributeRequestHeadersUserAgent - attributeRequestHeadersReferer - attributeRequestURI - attributeResponseHeadersContentType - attributeResponseHeadersContentLength - attributeResponseCode - AttributeAWSRequestID - AttributeAWSLambdaARN - AttributeAWSLambdaColdStart - AttributeAWSLambdaEventSourceARN - AttributeMessageRoutingKey - AttributeMessageQueueName - AttributeMessageExchangeType - AttributeMessageReplyTo - AttributeMessageCorrelationID -) - -// SpanAttribute is an attribute put in span events. -type SpanAttribute string - -// AddAgentSpanAttributer should be implemented by the Transaction. -type AddAgentSpanAttributer interface { - AddAgentSpanAttribute(key SpanAttribute, val string) -} - -// AddAgentSpanAttribute allows instrumentation packages to add span attributes. -func AddAgentSpanAttribute(txn interface{}, key SpanAttribute, val string) { - if aa, ok := txn.(AddAgentSpanAttributer); ok { - aa.AddAgentSpanAttribute(key, val) - } -} - -// These span event string constants must match the contents of the top level -// attributes.go file. -const ( - spanAttributeDBStatement SpanAttribute = "db.statement" - spanAttributeDBInstance SpanAttribute = "db.instance" - spanAttributeDBCollection SpanAttribute = "db.collection" - spanAttributePeerAddress SpanAttribute = "peer.address" - spanAttributePeerHostname SpanAttribute = "peer.hostname" - spanAttributeHTTPURL SpanAttribute = "http.url" - spanAttributeHTTPMethod SpanAttribute = "http.method" - // query parameters only appear in segments, not span events, but is - // listed as span attributes to simplify code. - spanAttributeQueryParameters SpanAttribute = "query_parameters" - // These span attributes are added by aws sdk instrumentation. - // https://source.datanerd.us/agents/agent-specs/blob/master/implementation_guides/aws-sdk.md#span-and-segment-attributes - SpanAttributeAWSOperation SpanAttribute = "aws.operation" - SpanAttributeAWSRequestID SpanAttribute = "aws.requestId" - SpanAttributeAWSRegion SpanAttribute = "aws.region" -) - -func (sa SpanAttribute) String() string { return string(sa) } - -var ( - usualDests = DestAll &^ destBrowser - tracesDests = destTxnTrace | destError - agentAttributeInfo = map[AgentAttributeID]struct { - name string - defaultDests destinationSet - }{ - AttributeHostDisplayName: {name: "host.displayName", defaultDests: usualDests}, - attributeRequestMethod: {name: "request.method", defaultDests: usualDests}, - attributeRequestAcceptHeader: {name: "request.headers.accept", defaultDests: usualDests}, - attributeRequestContentType: {name: "request.headers.contentType", defaultDests: usualDests}, - attributeRequestContentLength: {name: "request.headers.contentLength", defaultDests: usualDests}, - attributeRequestHeadersHost: {name: "request.headers.host", defaultDests: usualDests}, - attributeRequestHeadersUserAgent: {name: "request.headers.User-Agent", defaultDests: tracesDests}, - attributeRequestHeadersReferer: {name: "request.headers.referer", defaultDests: tracesDests}, - attributeRequestURI: {name: "request.uri", defaultDests: usualDests}, - attributeResponseHeadersContentType: {name: "response.headers.contentType", defaultDests: usualDests}, - attributeResponseHeadersContentLength: {name: "response.headers.contentLength", defaultDests: usualDests}, - attributeResponseCode: {name: "httpResponseCode", defaultDests: usualDests}, - AttributeAWSRequestID: {name: "aws.requestId", defaultDests: usualDests}, - AttributeAWSLambdaARN: {name: "aws.lambda.arn", defaultDests: usualDests}, - AttributeAWSLambdaColdStart: {name: "aws.lambda.coldStart", defaultDests: usualDests}, - AttributeAWSLambdaEventSourceARN: {name: "aws.lambda.eventSource.arn", defaultDests: usualDests}, - AttributeMessageRoutingKey: {name: "message.routingKey", defaultDests: usualDests}, - AttributeMessageQueueName: {name: "message.queueName", defaultDests: usualDests}, - AttributeMessageExchangeType: {name: "message.exchangeType", defaultDests: destNone}, - AttributeMessageReplyTo: {name: "message.replyTo", defaultDests: destNone}, - AttributeMessageCorrelationID: {name: "message.correlationId", defaultDests: destNone}, - } - spanAttributes = []SpanAttribute{ - spanAttributeDBStatement, - spanAttributeDBInstance, - spanAttributeDBCollection, - spanAttributePeerAddress, - spanAttributePeerHostname, - spanAttributeHTTPURL, - spanAttributeHTTPMethod, - spanAttributeQueryParameters, - SpanAttributeAWSOperation, - SpanAttributeAWSRequestID, - SpanAttributeAWSRegion, - } -) - -func (id AgentAttributeID) name() string { return agentAttributeInfo[id].name } - -// https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md - -// AttributeDestinationConfig matches newrelic.AttributeDestinationConfig to -// avoid circular dependency issues. -type AttributeDestinationConfig struct { - Enabled bool - Include []string - Exclude []string -} - -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[AgentAttributeID]destinationSet - spanDests map[SpanAttribute]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}) - } -} - -// AttributeConfigInput is used as the input to CreateAttributeConfig: it -// transforms newrelic.Config settings into an AttributeConfig. -type AttributeConfigInput struct { - Attributes AttributeDestinationConfig - ErrorCollector AttributeDestinationConfig - TransactionEvents AttributeDestinationConfig - BrowserMonitoring AttributeDestinationConfig - TransactionTracer AttributeDestinationConfig - SpanEvents AttributeDestinationConfig - TraceSegments AttributeDestinationConfig -} - -var ( - sampleAttributeConfigInput = AttributeConfigInput{ - Attributes: AttributeDestinationConfig{Enabled: true}, - ErrorCollector: AttributeDestinationConfig{Enabled: true}, - TransactionEvents: AttributeDestinationConfig{Enabled: true}, - TransactionTracer: AttributeDestinationConfig{Enabled: true}, - BrowserMonitoring: AttributeDestinationConfig{Enabled: true}, - SpanEvents: AttributeDestinationConfig{Enabled: true}, - TraceSegments: AttributeDestinationConfig{Enabled: true}, - } -) - -// CreateAttributeConfig creates a new AttributeConfig. -func CreateAttributeConfig(input AttributeConfigInput, 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, destError) - processDest(c, includeEnabled, &input.TransactionEvents, destTxnEvent) - processDest(c, includeEnabled, &input.TransactionTracer, destTxnTrace) - processDest(c, includeEnabled, &input.BrowserMonitoring, destBrowser) - processDest(c, includeEnabled, &input.SpanEvents, destSpan) - processDest(c, includeEnabled, &input.TraceSegments, destSegment) - - sort.Sort(byMatch(c.wildcardModifiers)) - - c.agentDests = make(map[AgentAttributeID]destinationSet) - for id, info := range agentAttributeInfo { - c.agentDests[id] = applyAttributeConfig(c, info.name, info.defaultDests) - } - c.spanDests = make(map[SpanAttribute]destinationSet, len(spanAttributes)) - for _, id := range spanAttributes { - c.spanDests[id] = applyAttributeConfig(c, id.String(), destSpan|destSegment) - } - - return c -} - -type userAttribute struct { - value interface{} - dests destinationSet -} - -type agentAttributeValue struct { - stringVal string - otherVal interface{} -} - -type agentAttributes map[AgentAttributeID]agentAttributeValue - -func (a *Attributes) filterSpanAttributes(s map[SpanAttribute]jsonWriter, d destinationSet) map[SpanAttribute]jsonWriter { - if nil != a { - for key := range s { - if a.config.spanDests[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 AgentAttributeID, d destinationSet) (string, interface{}) { - if nil == a || 0 == a.config.agentDests[id]&d { - return "", nil - } - v, _ := a.Agent[id] - return v.stringVal, v.otherVal -} - -// AddAgentAttributer allows instrumentation to add agent attributes without -// exposing a Transaction method. -type AddAgentAttributer interface { - AddAgentAttribute(id AgentAttributeID, stringVal string, otherVal interface{}) -} - -// 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 AgentAttributeID, stringVal string, otherVal interface{}) { - if "" != stringVal || otherVal != nil { - attr[id] = agentAttributeValue{ - stringVal: truncateStringValueIfLong(stringVal), - otherVal: otherVal, - } - } -} - -// 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) -} - -func truncateStringValueIfLong(val string) string { - if len(val) > attributeValueLengthLimit { - return StringLengthByteLimit(val, attributeValueLengthLimit) - } - return val -} - -// ValidateUserAttribute validates a user attribute. -func ValidateUserAttribute(key string, val interface{}) (interface{}, error) { - if str, ok := val.(string); ok { - val = interface{}(truncateStringValueIfLong(str)) - } - - switch val.(type) { - case string, bool, - uint8, uint16, uint32, uint64, int8, int16, int32, int64, - float32, float64, uint, int, uintptr: - 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 -} - -// 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: - w.stringField(key, v) - 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) - default: - w.stringField(key, fmt.Sprintf("%T", v)) - } -} - -func agentAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) { - if nil == a { - buf.WriteString("{}") - return - } - w := jsonFieldsWriter{buf: buf} - buf.WriteByte('{') - for id, val := range a.Agent { - if 0 != a.config.agentDests[id]&d { - if val.stringVal != "" { - w.stringField(id.name(), val.stringVal) - } else { - writeAttributeValueJSON(&w, id.name(), val.otherVal) - } - } - } - 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 0 != outputDest&d { - writeAttributeValueJSON(&w, key, val) - } - } - for name, atr := range a.user { - if 0 != atr.dests&d { - 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, h http.Header, u *url.URL) { - a.Agent.Add(attributeRequestMethod, method, nil) - - if nil != u { - a.Agent.Add(attributeRequestURI, SafeURL(u), nil) - } - - if nil == h { - return - } - a.Agent.Add(attributeRequestAcceptHeader, h.Get("Accept"), nil) - a.Agent.Add(attributeRequestContentType, h.Get("Content-Type"), nil) - a.Agent.Add(attributeRequestHeadersHost, h.Get("Host"), nil) - a.Agent.Add(attributeRequestHeadersUserAgent, h.Get("User-Agent"), nil) - a.Agent.Add(attributeRequestHeadersReferer, SafeURLFromString(h.Get("Referer")), nil) - - if l := GetContentLengthFromHeader(h); 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(attributeResponseHeadersContentType, h.Get("Content-Type"), nil) - - if l := GetContentLengthFromHeader(h); l >= 0 { - a.Agent.Add(attributeResponseHeadersContentLength, "", 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, rc, nil) -} diff --git a/internal/attributes_test.go b/internal/attributes_test.go deleted file mode 100644 index bf35801a3..000000000 --- a/internal/attributes_test.go +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "encoding/json" - "net/http" - "strconv" - "strings" - "testing" - - "github.com/newrelic/go-agent/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 - } - - input := AttributeConfigInput{ - Attributes: AttributeDestinationConfig{ - Enabled: tc.Config.AttributesEnabled, - Include: tc.Config.AttributesInclude, - Exclude: tc.Config.AttributesExclude, - }, - ErrorCollector: AttributeDestinationConfig{ - Enabled: tc.Config.ErrorAttributesEnabled, - Include: tc.Config.ErrorAttributesInclude, - Exclude: tc.Config.ErrorAttributesExclude, - }, - TransactionEvents: AttributeDestinationConfig{ - Enabled: tc.Config.EventsAttributesEnabled, - Include: tc.Config.EventsAttributesInclude, - Exclude: tc.Config.EventsAttributesExclude, - }, - BrowserMonitoring: AttributeDestinationConfig{ - Enabled: tc.Config.BrowserAttributesEnabled, - Include: tc.Config.BrowserAttributesInclude, - Exclude: tc.Config.BrowserAttributesExclude, - }, - TransactionTracer: AttributeDestinationConfig{ - Enabled: tc.Config.TracerAttributesEnabled, - Include: tc.Config.TracerAttributesInclude, - Exclude: tc.Config.TracerAttributesExclude, - }, - } - - cfg := CreateAttributeConfig(input, 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)) - 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 - }`) - 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(sampleAttributeConfigInput, 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(sampleAttributeConfigInput, 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(sampleAttributeConfigInput, 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(sampleAttributeConfigInput, 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(sampleAttributeConfigInput, 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 := sampleAttributeConfigInput - 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(sampleAttributeConfigInput, 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("Host", "the-host") - req.Header.Set("User-Agent", "the-agent") - req.Header.Set("Referer", "http://www.example.com") - req.Header.Set("Content-Length", "123") - - cfg := CreateAttributeConfig(sampleAttributeConfigInput, true) - - attrs := NewAttributes(cfg) - RequestAgentAttributes(attrs, req.Method, req.Header, req.URL) - 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.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(sampleAttributeConfigInput, 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("Host", "zap") - req.Header.Set("User-Agent", "zap") - req.Header.Set("Referer", "http://www.newrelic.com") - req.Header.Set("Content-Length", "123") - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - attrs := NewAttributes(cfg) - RequestAgentAttributes(attrs, req.Method, req.Header, req.URL) - 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 := sampleAttributeConfigInput - c.TransactionTracer.Exclude = []string{"request.uri"} - cfg := CreateAttributeConfig(c, true) - attrs = NewAttributes(cfg) - attrs.Agent.Add(attributeResponseHeadersContentLength, "", 123) - attrs.Agent.Add(attributeRequestMethod, "GET", nil) - attrs.Agent.Add(attributeRequestURI, "/url", nil) // disabled by configuration - - outstr, outother = attrs.GetAgentValue(attributeResponseHeadersContentLength, 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) - } -} diff --git a/internal/browser.go b/internal/browser.go deleted file mode 100644 index 82ffa71e5..000000000 --- a/internal/browser.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import "bytes" - -// 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() -} diff --git a/internal/browser_test.go b/internal/browser_test.go deleted file mode 100644 index 7806755ee..000000000 --- a/internal/browser_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "testing" -) - -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) { - a := NewAttributes(CreateAttributeConfig(sampleAttributeConfigInput, 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) - } -} diff --git a/internal/cat/appdata.go b/internal/cat/appdata.go deleted file mode 100644 index f0cd3766d..000000000 --- a/internal/cat/appdata.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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/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 -} diff --git a/internal/cat/appdata_test.go b/internal/cat/appdata_test.go deleted file mode 100644 index 1289e4f1c..000000000 --- a/internal/cat/appdata_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// 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) - } - } -} diff --git a/internal/cat/errors.go b/internal/cat/errors.go deleted file mode 100644 index 2f516ed05..000000000 --- a/internal/cat/errors.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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) -} diff --git a/internal/cat/headers.go b/internal/cat/headers.go deleted file mode 100644 index 6ca05cd67..000000000 --- a/internal/cat/headers.go +++ /dev/null @@ -1,16 +0,0 @@ -// 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" -) diff --git a/internal/cat/id.go b/internal/cat/id.go deleted file mode 100644 index 766eb3def..000000000 --- a/internal/cat/id.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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 -} diff --git a/internal/cat/id_test.go b/internal/cat/id_test.go deleted file mode 100644 index 190463996..000000000 --- a/internal/cat/id_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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) - } - } -} diff --git a/internal/cat/path_hash.go b/internal/cat/path_hash.go deleted file mode 100644 index edb483a33..000000000 --- a/internal/cat/path_hash.go +++ /dev/null @@ -1,38 +0,0 @@ -// 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 -} diff --git a/internal/cat/path_hash_test.go b/internal/cat/path_hash_test.go deleted file mode 100644 index 1d7bf08e0..000000000 --- a/internal/cat/path_hash_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package cat - -import ( - "testing" - - "github.com/newrelic/go-agent/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) - } - } -} diff --git a/internal/cat/synthetics.go b/internal/cat/synthetics.go deleted file mode 100644 index b88c15476..000000000 --- a/internal/cat/synthetics.go +++ /dev/null @@ -1,85 +0,0 @@ -// 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 -} - -var ( - errInvalidSyntheticsJSON = errors.New("invalid synthetics 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") -) - -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 -} diff --git a/internal/cat/synthetics_test.go b/internal/cat/synthetics_test.go deleted file mode 100644 index 120d1f53c..000000000 --- a/internal/cat/synthetics_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package cat - -import ( - "encoding/json" - "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) - } - } -} diff --git a/internal/cat/txndata.go b/internal/cat/txndata.go deleted file mode 100644 index 5a589af57..000000000 --- a/internal/cat/txndata.go +++ /dev/null @@ -1,99 +0,0 @@ -// 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/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 -} diff --git a/internal/cat/txndata_test.go b/internal/cat/txndata_test.go deleted file mode 100644 index 4bb01fdfb..000000000 --- a/internal/cat/txndata_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// 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) - } - } -} diff --git a/internal/cat_test.go b/internal/cat_test.go deleted file mode 100644 index a3960c47d..000000000 --- a/internal/cat_test.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/newrelic/go-agent/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, &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]) - } - } - } -} diff --git a/internal/collector.go b/internal/collector.go deleted file mode 100644 index 73285ff14..000000000 --- a/internal/collector.go +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "regexp" - "strconv" - "time" - - "github.com/newrelic/go-agent/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" - 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 - AgentVersion string -} - -// 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 -} - -func newRPMResponse(statusCode int) RPMResponse { - var err error - if statusCode != 200 && statusCode != 202 { - err = fmt.Errorf("response code: %d", statusCode) - } - return RPMResponse{statusCode: statusCode, Err: err} -} - -// 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 -} - -// ShouldSaveHarvestData indicates that the agent should save the data and try -// to send it in the next harvest. -func (resp RPMResponse) ShouldSaveHarvestData() bool { - 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 collectorRequestInternal(url string, cmd RpmCmd, cs RpmControls) RPMResponse { - compressed, err := compress(cmd.Data) - if nil != err { - return RPMResponse{Err: err} - } - - if l := compressed.Len(); l > cmd.MaxPayloadSize { - return RPMResponse{Err: 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 RPMResponse{Err: err} - } - - req.Header.Add("Accept-Encoding", "identity, deflate") - req.Header.Add("Content-Type", "application/octet-stream") - req.Header.Add("User-Agent", userAgentPrefix+cs.AgentVersion) - 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 RPMResponse{Err: err} - } - - defer resp.Body.Close() - - r := newRPMResponse(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 := ioutil.ReadAll(resp.Body) - if nil == r.Err { - r.Err = err - } - r.body = body - - return r -} - -// CollectorRequest makes a request to New Relic. -func CollectorRequest(cmd RpmCmd, cs RpmControls) RPMResponse { - url := rpmURL(cmd, cs) - - if cs.Logger.DebugEnabled() { - cs.Logger.Debug("rpm request", map[string]interface{}{ - "command": cmd.Name, - "url": url, - "payload": JSONString(cmd.Data), - }) - } - - resp := collectorRequestInternal(url, cmd, cs) - - if cs.Logger.DebugEnabled() { - if err := resp.Err; err != nil { - cs.Logger.Debug("rpm failure", map[string]interface{}{ - "command": cmd.Name, - "url": url, - "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": url, - "response": JSONString(resp.body), - }) - } - } - - return resp -} - -const ( - // NEW_RELIC_HOST can be used to override the New Relic endpoint. This - // is useful for testing. - envHost = "NEW_RELIC_HOST" -) - -var ( - preconnectHostOverride = os.Getenv(envHost) - preconnectHostDefault = "collector.newrelic.com" - preconnectRegionLicenseRegex = regexp.MustCompile(`(^.+?)x`) -) - -func calculatePreconnectHost(license, overrideHost string) string { - if "" != overrideHost { - return overrideHost - } - m := preconnectRegionLicenseRegex.FindStringSubmatch(license) - if len(m) > 1 { - return "collector." + m[1] + ".nr-data.net" - } - return preconnectHostDefault -} - -// ConnectJSONCreator allows the creation of the connect payload JSON to be -// deferred until the SecurityPolicies are acquired and vetted. -type ConnectJSONCreator interface { - CreateConnectJSON(*SecurityPolicies) ([]byte, error) -} - -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 ConnectJSONCreator, securityPoliciesToken string, highSecurity bool, cs RpmControls) (*ConnectReply, RPMResponse) { - preconnectData, err := json.Marshal([]preconnectRequest{{ - SecurityPoliciesToken: securityPoliciesToken, - HighSecurity: highSecurity, - }}) - if nil != err { - return nil, RPMResponse{Err: fmt.Errorf("unable to marshal preconnect data: %v", err)} - } - - call := RpmCmd{ - Name: cmdPreconnect, - Collector: calculatePreconnectHost(cs.License, preconnectHostOverride), - Data: preconnectData, - MaxPayloadSize: maxPayloadSizeInBytes, - } - - resp := CollectorRequest(call, cs) - if nil != resp.Err { - return nil, resp - } - - var preconnect struct { - Preconnect PreconnectReply `json:"return_value"` - } - err = json.Unmarshal(resp.body, &preconnect) - if nil != err { - // Certain security policy errors must be treated as a disconnect. - return nil, RPMResponse{ - Err: fmt.Errorf("unable to process preconnect reply: %v", err), - disconnectSecurityPolicy: isDisconnectSecurityPolicyError(err), - } - } - - js, err := config.CreateConnectJSON(preconnect.Preconnect.SecurityPolicies.PointerIfPopulated()) - if nil != err { - return nil, RPMResponse{Err: 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 nil != resp.Err { - return nil, resp - } - - reply, err := ConstructConnectReply(resp.body, preconnect.Preconnect) - if nil != err { - return nil, RPMResponse{Err: 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, RPMResponse{Err: errMissingAgentRunID} - } - - return reply, resp -} - -// ConstructConnectReply takes the body of a Connect reply, in the form of bytes, and a -// PreconnectReply, and converts it into a *ConnectReply -func ConstructConnectReply(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 - - reply.Reply.AdaptiveSampler = NewAdaptiveSampler( - time.Duration(reply.Reply.SamplingTargetPeriodInSeconds)*time.Second, - reply.Reply.SamplingTarget, - time.Now()) - reply.Reply.rulesCache = newRulesCache(txnNameCacheLimit) - - return reply.Reply, nil -} diff --git a/internal/collector_test.go b/internal/collector_test.go deleted file mode 100644 index bbf97589f..000000000 --- a/internal/collector_test.go +++ /dev/null @@ -1,562 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - "testing" - - "github.com/newrelic/go-agent/internal/crossagent" - "github.com/newrelic/go-agent/internal/logger" -) - -func TestResponseCodeError(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(tc.code) - if tc.success != (nil == resp.Err) { - t.Error("error", tc.code, tc.success, resp.Err) - } - if tc.disconnect != resp.IsDisconnect() { - t.Error("disconnect", tc.code, tc.disconnect, resp.Err) - } - if tc.restart != resp.IsRestartException() { - t.Error("restart", tc.code, tc.restart, resp.Err) - } - if tc.saveHarvestData != resp.ShouldSaveHarvestData() { - t.Error("save harvest data", tc.code, tc.saveHarvestData, resp.Err) - } - } -} - -type roundTripperFunc func(*http.Request) (*http.Response, error) - -func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { - return fn(r) -} - -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: 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/agent_version") - testField("Content-Encoding", r.Header.Get("Content-Encoding"), "gzip") - testField("zip", r.Header.Get("zip"), "zap") - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader("body")), - }, nil - }), - }, - Logger: logger.ShimLogger{IsDebugEnabled: true}, - AgentVersion: "agent_version", - } - resp := CollectorRequest(cmd, cs) - if nil != resp.Err { - t.Error(resp.Err) - } -} - -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: ioutil.NopCloser(strings.NewReader("body")), - }, nil - }), - }, - Logger: logger.ShimLogger{IsDebugEnabled: true}, - AgentVersion: "agent_version", - } - u := ":" // bad url - resp := collectorRequestInternal(u, cmd, cs) - if nil == resp.Err { - t.Error("missing expected error") - } - -} - -func TestUrl(t *testing.T) { - cmd := RpmCmd{ - Name: "foo_method", - Collector: "example.com", - } - cs := RpmControls{ - License: "123abc", - Client: nil, - Logger: nil, - AgentVersion: "1", - } - - 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: ioutil.NopCloser(strings.NewReader(body)), - } -} - -type endpointResult struct { - response *http.Response - err error -} - -type connectMock struct { - redirect endpointResult - connect endpointResult - // testConfig will be used if this is nil - config ConnectJSONCreator -} - -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) {} - -type testConfig struct{} - -func (tc testConfig) CreateConnectJSON(*SecurityPolicies) ([]byte, error) { - return []byte(`"connect-json"`), nil -} - -type errorConfig struct{} - -func (c errorConfig) CreateConnectJSON(*SecurityPolicies) ([]byte, error) { - return nil, errors.New("error creating config JSON") -} - -func testConnectHelper(cm connectMock) (*ConnectReply, RPMResponse) { - config := cm.config - if nil == config { - config = testConfig{} - } - cs := RpmControls{ - License: "12345", - Client: &http.Client{Transport: cm}, - Logger: logger.ShimLogger{IsDebugEnabled: true}, - AgentVersion: "1", - } - - return ConnectAttempt(config, "", false, 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.Err { - t.Fatal(run, resp.Err) - } - 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.Err == nil { - t.Fatal("missing expected error") - } -} - -func TestConnectConfigJSONError(t *testing.T) { - run, resp := testConnectHelper(connectMock{ - redirect: endpointResult{response: makeResponse(200, redirectBody)}, - connect: endpointResult{response: makeResponse(200, connectBody)}, - config: errorConfig{}, - }) - if nil != run { - t.Fatal(run) - } - if resp.Err == 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.Err { - 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.Err { - 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.Err { - 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.Err { - 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.Err { - 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.Err { - 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.Err { - 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.Err { - t.Fatal("wrong error", resp.Err) - } -} - -func TestCalculatePreconnectHost(t *testing.T) { - // non-region license - host := calculatePreconnectHost("0123456789012345678901234567890123456789", "") - if host != preconnectHostDefault { - t.Error(host) - } - // override present - override := "other-collector.newrelic.com" - host = calculatePreconnectHost("0123456789012345678901234567890123456789", override) - if host != override { - t.Error(host) - } - // four letter region - host = calculatePreconnectHost("eu01xx6789012345678901234567890123456789", "") - if host != "collector.eu01.nr-data.net" { - t.Error(host) - } - // five letter region - host = calculatePreconnectHost("gov01x6789012345678901234567890123456789", "") - if host != "collector.gov01.nr-data.net" { - t.Error(host) - } - // six letter region - host = calculatePreconnectHost("foo001x6789012345678901234567890123456789", "") - if host != "collector.foo001.nr-data.net" { - t.Error(host) - } -} - -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 - } - - host := calculatePreconnectHost(configKey, overrideHost) - if host != tc.ExpectHostname { - t.Errorf(`test="%s" got="%s" expected="%s"`, tc.Name, host, tc.ExpectHostname) - } - } -} - -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}, - } - resp := CollectorRequest(cmd, cs) - if nil == resp.Err { - 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: ioutil.NopCloser(strings.NewReader(replyBody)), - }, nil - }), - }, - Logger: logger.ShimLogger{IsDebugEnabled: true}, - } - } - - for _, test := range testcases { - reply, resp := ConnectAttempt(testConfig{}, "", false, controls(test.replyBody)) - if nil != resp.Err { - t.Error("resp returned unexpected error:", resp.Err) - } - 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) - } - } -} diff --git a/internal/compress.go b/internal/compress.go deleted file mode 100644 index 2347ca199..000000000 --- a/internal/compress.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "compress/gzip" -) - -func compress(b []byte) (*bytes.Buffer, error) { - var buf bytes.Buffer - w := gzip.NewWriter(&buf) - _, err := w.Write(b) - w.Close() - - if nil != err { - return nil, err - } - - return &buf, nil -} diff --git a/internal/connect_reply.go b/internal/connect_reply.go deleted file mode 100644 index 2e318d220..000000000 --- a/internal/connect_reply.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "strings" - "time" -) - -// AgentRunID identifies the current connection with the collector. -type AgentRunID string - -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"` - - AdaptiveSampler AdaptiveSampler - // 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:"-"` - - // 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"` - - // rulesCache caches the results of calling CreateFullTxnName. It - // exists here in ConnectReply since it is specific to a set of rules - // and is shared between transactions. - rulesCache *rulesCache - - 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"` - CrossApplicationTracerEnabled *bool `json:"cross_application_tracer.enabled"` - } `json:"agent_config"` - - // Faster Event Harvest - EventData EventHarvestConfig `json:"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"` - ErrorEvents *uint `json:"error_event_data,omitempty"` - SpanEvents *uint `json:"span_event_data,omitempty"` - } `json:"harvest_limits"` -} - -// 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(eventer MaxTxnEventer) EventHarvestConfig { - cfg := EventHarvestConfig{} - cfg.ReportPeriodMs = DefaultConfigurableEventHarvestMs - cfg.Limits.TxnEvents = uintPtr(uint(eventer.MaxTxnEvents())) - cfg.Limits.CustomEvents = uintPtr(uint(MaxCustomEvents)) - cfg.Limits.ErrorEvents = uintPtr(uint(MaxErrorEvents)) - return cfg -} - -type trustedAccountSet map[int]struct{} - -func (t *trustedAccountSet) IsTrusted(account int) bool { - _, exists := (*t)[account] - return exists -} - -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, - // No transactions should be sampled before the application is - // connected. - AdaptiveSampler: SampleNothing{}, - - SamplingTarget: 10, - SamplingTargetPeriodInSeconds: 60, - - TraceIDGenerator: NewTraceIDGenerator(int64(time.Now().UnixNano())), - } -} - -// 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) -} - -// 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 { - if name := reply.rulesCache.find(input, isWeb); "" != name { - return name - } - name := constructFullTxnName(input, 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. - reply.rulesCache.set(input, isWeb, name) - } - return name -} - -func constructFullTxnName(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) -} diff --git a/internal/connect_reply_test.go b/internal/connect_reply_test.go deleted file mode 100644 index 1261e8bc4..000000000 --- a/internal/connect_reply_test.go +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "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 TestCreateFullTxnNameAllRulesWithCache(t *testing.T) { - js := `{ - "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"]} - ] - }` - reply := ConnectReplyDefaults() - reply.rulesCache = newRulesCache(3) - err := json.Unmarshal([]byte(js), &reply) - if nil != err { - t.Fatal(err) - } - want := "WebTransaction/Go/zap/zoop/*/zyp" - if out := CreateFullTxnName("/zap/zip/zep", reply, true); out != want { - t.Error("wanted:", want, "got:", out) - } - // Check that the cache was populated as expected. - if out := reply.rulesCache.find("/zap/zip/zep", true); out != want { - t.Error("wanted:", want, "got:", out) - } - // Check that the next CreateFullTxnName returns the same output. - if out := CreateFullTxnName("/zap/zip/zep", reply, true); out != want { - t.Error("wanted:", want, "got:", 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() - reply.rulesCache = newRulesCache(1) - 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 := ConstructConnectReply([]byte(`{"return_value":{ - "event_harvest_config": { - "harvest_limits": { - "error_event_data": -1 - } - } - }}`), PreconnectReply{}) - if err == nil { - t.Fatal("expected error missing") - } -} - -type dfltMaxTxnEvents struct{} - -func (dfltMaxTxnEvents) MaxTxnEvents() int { - return MaxTxnEvents -} - -func TestDefaultEventHarvestConfigJSON(t *testing.T) { - js, err := json.Marshal(DefaultEventHarvestConfig(dfltMaxTxnEvents{})) - if err != nil { - t.Error(err) - } - if string(js) != `{"report_period_ms":60000,"harvest_limits":{"analytic_event_data":10000,"custom_event_data":10000,"error_event_data":100}}` { - t.Error(string(js)) - } -} diff --git a/internal/context.go b/internal/context.go deleted file mode 100644 index cd7ef2468..000000000 --- a/internal/context.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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" -) diff --git a/internal/cross_process_http.go b/internal/cross_process_http.go deleted file mode 100644 index 93b74e5dd..000000000 --- a/internal/cross_process_http.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/http" - - "github.com/newrelic/go-agent/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), - } -} - -// 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) - } - - return header -} diff --git a/internal/cross_process_http_test.go b/internal/cross_process_http_test.go deleted file mode 100644 index 1d3409eb5..000000000 --- a/internal/cross_process_http_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/http" - "reflect" - "testing" - - "github.com/newrelic/go-agent/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) - } -} diff --git a/internal/crossagent/README.md b/internal/crossagent/README.md deleted file mode 100644 index 49b233213..000000000 --- a/internal/crossagent/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Cross Agent Tests - -At commit a4ec8e617340c8c7936d15ad18309ff5b9cfa93e. diff --git a/internal/crossagent/cross_agent_tests/README.md b/internal/crossagent/cross_agent_tests/README.md deleted file mode 100644 index 459c5023b..000000000 --- a/internal/crossagent/cross_agent_tests/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 cross-agent-team GHE group. Contact Belinda Runkle if you are on the agent team but don't have push access. - -### 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 | diff --git a/internal/crossagent/cross_agent_tests/attribute_configuration.json b/internal/crossagent/cross_agent_tests/attribute_configuration.json deleted file mode 100644 index c2b68673b..000000000 --- a/internal/crossagent/cross_agent_tests/attribute_configuration.json +++ /dev/null @@ -1,630 +0,0 @@ -[{ - "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" - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/cat/README.md b/internal/crossagent/cross_agent_tests/cat/README.md deleted file mode 100644 index 60a88c1d4..000000000 --- a/internal/crossagent/cross_agent_tests/cat/README.md +++ /dev/null @@ -1,28 +0,0 @@ -### 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. | diff --git a/internal/crossagent/cross_agent_tests/cat/cat_map.json b/internal/crossagent/cross_agent_tests/cat/cat_map.json deleted file mode 100644 index 15c33ba88..000000000 --- a/internal/crossagent/cross_agent_tests/cat/cat_map.json +++ /dev/null @@ -1,595 +0,0 @@ -[ - { - "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" - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/cat/path_hashing.json b/internal/crossagent/cross_agent_tests/cat/path_hashing.json deleted file mode 100644 index 2bc760d29..000000000 --- a/internal/crossagent/cross_agent_tests/cat/path_hashing.json +++ /dev/null @@ -1,51 +0,0 @@ -[ - { - "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" - } -] diff --git a/internal/crossagent/cross_agent_tests/collector_hostname.json b/internal/crossagent/cross_agent_tests/collector_hostname.json deleted file mode 100644 index ff381c889..000000000 --- a/internal/crossagent/cross_agent_tests/collector_hostname.json +++ /dev/null @@ -1,71 +0,0 @@ -[ - { - "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" - } -] diff --git a/internal/crossagent/cross_agent_tests/data_collection_server_configuration.json b/internal/crossagent/cross_agent_tests/data_collection_server_configuration.json deleted file mode 100644 index 7a0452240..000000000 --- a/internal/crossagent/cross_agent_tests/data_collection_server_configuration.json +++ /dev/null @@ -1,219 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/datastores/README.md b/internal/crossagent/cross_agent_tests/datastores/README.md deleted file mode 100644 index a38115b51..000000000 --- a/internal/crossagent/cross_agent_tests/datastores/README.md +++ /dev/null @@ -1,16 +0,0 @@ -## 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. diff --git a/internal/crossagent/cross_agent_tests/datastores/datastore_api.json b/internal/crossagent/cross_agent_tests/datastores/datastore_api.json deleted file mode 100644 index c07e2b6d2..000000000 --- a/internal/crossagent/cross_agent_tests/datastores/datastore_api.json +++ /dev/null @@ -1,443 +0,0 @@ -[ - { - "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" - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/datastores/datastore_instances.json b/internal/crossagent/cross_agent_tests/datastores/datastore_instances.json deleted file mode 100644 index 55dbd10c1..000000000 --- a/internal/crossagent/cross_agent_tests/datastores/datastore_instances.json +++ /dev/null @@ -1,73 +0,0 @@ -[ - { - "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" - } -] diff --git a/internal/crossagent/cross_agent_tests/distributed_tracing/distributed_tracing.json b/internal/crossagent/cross_agent_tests/distributed_tracing/distributed_tracing.json deleted file mode 100644 index 5487a3fa4..000000000 --- a/internal/crossagent/cross_agent_tests/distributed_tracing/distributed_tracing.json +++ /dev/null @@ -1,1281 +0,0 @@ -[ - { - "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": "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"], - "unexpected": ["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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "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", "parent.transportType", "parentId"] - } - }, - "expected_metrics": [ - ["Supportability/DistributedTrace/AcceptPayload/ParseException", 1] - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/README.md b/internal/crossagent/cross_agent_tests/docker_container_id/README.md deleted file mode 100644 index 0b69ba17f..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/README.md +++ /dev/null @@ -1,6 +0,0 @@ -These 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. diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/cases.json b/internal/crossagent/cross_agent_tests/docker_container_id/cases.json deleted file mode 100644 index 18c823cda..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/cases.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "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": "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 - } -] diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-0.9.1.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-0.9.1.txt deleted file mode 100644 index dfc8b8067..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-0.9.1.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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:/ \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.0.0.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.0.0.txt deleted file mode 100644 index 0e85d10ce..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.0.0.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-lxc-driver.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-lxc-driver.txt deleted file mode 100644 index f2c3d3dcc..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-lxc-driver.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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/cb8c113e5f3cf8332f5231f8154adc429ea910f7c29995372de4f571c55d3159 \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-fs.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-fs.txt deleted file mode 100644 index e29f420da..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-fs.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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:/ \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-systemd.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-systemd.txt deleted file mode 100644 index 7e7e1ce0d..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.1.2-native-driver-systemd.txt +++ /dev/null @@ -1,10 +0,0 @@ -10: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.scope \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.3.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.3.txt deleted file mode 100644 index 1ccd44343..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-1.3.txt +++ /dev/null @@ -1,9 +0,0 @@ -9: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-custom-prefix.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-custom-prefix.txt deleted file mode 100644 index 75a1aa4ce..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-custom-prefix.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/docker-too-long.txt b/internal/crossagent/cross_agent_tests/docker_container_id/docker-too-long.txt deleted file mode 100644 index 6eb1dcfb9..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/docker-too-long.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/empty.txt b/internal/crossagent/cross_agent_tests/docker_container_id/empty.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/heroku.txt b/internal/crossagent/cross_agent_tests/docker_container_id/heroku.txt deleted file mode 100644 index 7a1ae0d78..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/heroku.txt +++ /dev/null @@ -1 +0,0 @@ -1:hugetlb,perf_event,blkio,freezer,devices,memory,cpuacct,cpu,cpuset:/lxc/b6d196c1-50f2-4949-abdb-5d4909864487 diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/invalid-characters.txt b/internal/crossagent/cross_agent_tests/docker_container_id/invalid-characters.txt deleted file mode 100644 index 4ff2f23f0..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/invalid-characters.txt +++ /dev/null @@ -1,9 +0,0 @@ -9: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/invalid-length.txt b/internal/crossagent/cross_agent_tests/docker_container_id/invalid-length.txt deleted file mode 100644 index 8166b21fa..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/invalid-length.txt +++ /dev/null @@ -1,9 +0,0 @@ -9: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:/ diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/no_cpu_subsystem.txt b/internal/crossagent/cross_agent_tests/docker_container_id/no_cpu_subsystem.txt deleted file mode 100644 index 717b38bf7..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/no_cpu_subsystem.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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 diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-lxc-container.txt b/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-lxc-container.txt deleted file mode 100644 index 8c5b635a5..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-lxc-container.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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.session \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-no-container.txt b/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-no-container.txt deleted file mode 100644 index 4439bc556..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.04-no-container.txt +++ /dev/null @@ -1,10 +0,0 @@ -11: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.session \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.10-no-container.txt b/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.10-no-container.txt deleted file mode 100644 index fc9873642..000000000 --- a/internal/crossagent/cross_agent_tests/docker_container_id/ubuntu-14.10-no-container.txt +++ /dev/null @@ -1,10 +0,0 @@ -10: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.scope \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/labels.json b/internal/crossagent/cross_agent_tests/labels.json deleted file mode 100644 index fc2cb6cb5..000000000 --- a/internal/crossagent/cross_agent_tests/labels.json +++ /dev/null @@ -1,195 +0,0 @@ -[ - { - "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": "𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆𝌆"}] - } -] diff --git a/internal/crossagent/cross_agent_tests/language_agents_security_policies.json b/internal/crossagent/cross_agent_tests/language_agents_security_policies.json deleted file mode 100644 index cd1b5c75b..000000000 --- a/internal/crossagent/cross_agent_tests/language_agents_security_policies.json +++ /dev/null @@ -1,269 +0,0 @@ -[{ - "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 -}] diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/README.md b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/README.md deleted file mode 100644 index f839f9e6a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# 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). diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.colon_obfuscated.txt deleted file mode 100644 index 7ec5fb0e6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.colon_obfuscated.txt +++ /dev/null @@ -1,3 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: ? - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.explain.txt deleted file mode 100644 index 17756f238..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.explain.txt +++ /dev/null @@ -1,3 +0,0 @@ - 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) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.obfuscated.txt deleted file mode 100644 index 0302012b5..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.obfuscated.txt +++ /dev/null @@ -1,3 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: (id = ?) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.query.txt deleted file mode 100644 index 98504f2b1..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/basic_where.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.id=1234 AND blogs.title='sensitive text' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.colon_obfuscated.txt deleted file mode 100644 index 39cf4175e..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.explain.txt deleted file mode 100644 index 8005a6395..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) - Filter: ((j = 'a'::"char") AND (k = ('now'::cstring)::date)) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.obfuscated.txt deleted file mode 100644 index d83600f81..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..56.60 rows=1 width=5) - Filter: ((j = ?::"char") AND (k = (?::cstring)::date)) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.query.txt deleted file mode 100644 index 235ec0bb1..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/current_date.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain select * from explain_plan_test_4 where j = 'abcd' and k = current_date diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.colon_obfuscated.txt deleted file mode 100644 index b3cafd41b..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.explain.txt deleted file mode 100644 index dac8471e3..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) - Filter: (k = '2001-09-28'::date) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.obfuscated.txt deleted file mode 100644 index bbf32e474..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_4 (cost=0.00..39.12 rows=12 width=5) - Filter: (k = ?::date) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.query.txt deleted file mode 100644 index 8b13eac41..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/date.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain select * from explain_plan_test_4 where k = date '2001-09-28'" diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.colon_obfuscated.txt deleted file mode 100644 index 45c36b5e3..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on blogs (cost=0.00..1.01 rows=1 width=540) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.explain.txt deleted file mode 100644 index 6f154a24e..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.explain.txt +++ /dev/null @@ -1,3 +0,0 @@ -Seq Scan on blogs (cost=0.00..1.01 rows=1 width=540) - Filter: ((title)::text = '\x08\x0C - \r '::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.obfuscated.txt deleted file mode 100644 index bca13233a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on blogs (cost=0.00..1.01 rows=1 width=540) - Filter: ((title)::text = ?::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.query.txt deleted file mode 100644 index 230411c59..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_newline.query.txt +++ /dev/null @@ -1 +0,0 @@ -select * from blogs where title = E'\x08\x0c\n\r\t' diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.colon_obfuscated.txt deleted file mode 100644 index 4a95be3ef..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.explain.txt deleted file mode 100644 index 8b400e7d1..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) - Filter: (c = 'three''three'::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.obfuscated.txt deleted file mode 100644 index 3b90e9c81..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..24.50 rows=6 width=40) - Filter: (c = ?::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.query.txt deleted file mode 100644 index c3ddc49e7..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/embedded_quote.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain select * from explain_plan_test_1 where c = 'three''three' diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.colon_obfuscated.txt deleted file mode 100644 index c76e41e9a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.explain.txt deleted file mode 100644 index c9efd5484..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) - Filter: ((a)::numeric = 10000000000::numeric) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.obfuscated.txt deleted file mode 100644 index 7595d016a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on explain_plan_test_1 (cost=0.00..27.40 rows=6 width=40) - Filter: ((a)::numeric = ?::numeric) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.query.txt deleted file mode 100644 index 7f30e0b10..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/floating_point.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain select * from explain_plan_test_1 where a = 1e10 diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.colon_obfuscated.txt deleted file mode 100644 index b699b0744..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.colon_obfuscated.txt +++ /dev/null @@ -1,5 +0,0 @@ - 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) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.explain.txt deleted file mode 100644 index 7b7b2fa64..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.explain.txt +++ /dev/null @@ -1,5 +0,0 @@ - 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) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.obfuscated.txt deleted file mode 100644 index ac0e0ee83..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.obfuscated.txt +++ /dev/null @@ -1,5 +0,0 @@ - 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) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.query.txt deleted file mode 100644 index be642a22d..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/function_with_strings.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs JOIN posts ON posts.title=CONCAT(blogs.title, '-suffix') \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.colon_obfuscated.txt deleted file mode 100644 index 7e5d01c89..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) - Filter: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.explain.txt deleted file mode 100644 index ffd06faaf..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) - Filter: (i = '"abcd"'::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.obfuscated.txt deleted file mode 100644 index cdfec9a89..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ -Seq Scan on "explain_plan_test'_3" (cost=0.00..24.50 rows=6 width=40) - Filter: (i = ?::text) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.query.txt deleted file mode 100644 index 01bfc59d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/quote_in_table_name.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain select * from "explain_plan_test'_3" where i = '"abcd"' diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.colon_obfuscated.txt deleted file mode 100644 index e2ad7da4d..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.colon_obfuscated.txt +++ /dev/null @@ -1,5 +0,0 @@ -Insert 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) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.explain.txt deleted file mode 100644 index 3f6d93dbc..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.explain.txt +++ /dev/null @@ -1,5 +0,0 @@ -Insert 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) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.obfuscated.txt deleted file mode 100644 index 3f6d93dbc..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.obfuscated.txt +++ /dev/null @@ -1,5 +0,0 @@ -Insert 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) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.query.txt deleted file mode 100644 index 4c63710b0..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/subplan.query.txt +++ /dev/null @@ -1 +0,0 @@ -explain 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) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.colon_obfuscated.txt deleted file mode 100644 index 28a296df9..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: ? diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.explain.txt deleted file mode 100644 index 904186fa3..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: (id = 1234) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.obfuscated.txt deleted file mode 100644 index bc1714cd2..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: (id = ?) diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.query.txt deleted file mode 100644 index bf9f428c4..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_integer.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.id=1234 diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.explain.txt deleted file mode 100644 index 7701fbb37..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = '][^|)/('::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.query.txt deleted file mode 100644 index d3216f3e5..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_regex_chars.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title='][^|)/(' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.colon_obfuscated.txt deleted file mode 100644 index 7ec5fb0e6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.colon_obfuscated.txt +++ /dev/null @@ -1,3 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: ? - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.explain.txt deleted file mode 100644 index c6a46a121..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.explain.txt +++ /dev/null @@ -1,3 +0,0 @@ - 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) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.obfuscated.txt deleted file mode 100644 index 0302012b5..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.obfuscated.txt +++ /dev/null @@ -1,3 +0,0 @@ - Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540) - Index Cond: (id = ?) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.query.txt deleted file mode 100644 index 32dca5b5b..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/where_with_substring.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.id=15402 AND blogs.title='logs' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.explain.txt deleted file mode 100644 index 79e1da81a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'foo''bar'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.query.txt deleted file mode 100644 index 2854fb92b..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case1.query.txt +++ /dev/null @@ -1 +0,0 @@ -EXPLAIN SELECT * FROM blogs WHERE blogs.title=E'foo\'bar' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.explain.txt deleted file mode 100644 index 956a40e8b..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.explain.txt +++ /dev/null @@ -1,3 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = '\x08\x0C - \r '::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.query.txt deleted file mode 100644 index 3dfab9ea7..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case2.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=E'\b\f\n\r\t' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.explain.txt deleted file mode 100644 index 896ebec6d..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = '\x01\x079'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.query.txt deleted file mode 100644 index 05ca5b98d..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case3.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=E'\1\7\9' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.explain.txt deleted file mode 100644 index 79e1da81a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'foo''bar'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.query.txt deleted file mode 100644 index 2247258da..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case4.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title='foo\'bar' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.explain.txt deleted file mode 100644 index b6b473025..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'U'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.query.txt deleted file mode 100644 index 4089e9be7..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case5.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=E'\x55' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.explain.txt deleted file mode 100644 index 37f3d9e7c..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'data'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.query.txt deleted file mode 100644 index f8ef48f2a..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case6.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=E'd\u0061t\U00000061' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.explain.txt deleted file mode 100644 index 37f3d9e7c..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'data'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.query.txt deleted file mode 100644 index ca20c583d..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case7.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=U&'d\0061t\+000061' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.explain.txt deleted file mode 100644 index 3941c5a10..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'слон'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.query.txt deleted file mode 100644 index 08c76cb94..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case8.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=U&'\0441\043B\043E\043D' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.colon_obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.colon_obfuscated.txt deleted file mode 100644 index c1159e0d6..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.colon_obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ? \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.explain.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.explain.txt deleted file mode 100644 index 37f3d9e7c..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.explain.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = 'data'::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.obfuscated.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.obfuscated.txt deleted file mode 100644 index 083affa27..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.obfuscated.txt +++ /dev/null @@ -1,2 +0,0 @@ - Seq Scan on blogs (cost=0.00..11.75 rows=1 width=540) - Filter: ((title)::text = ?::text) \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.query.txt b/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.query.txt deleted file mode 100644 index 69184644f..000000000 --- a/internal/crossagent/cross_agent_tests/postgres_explain_obfuscation/with_escape_case9.query.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT * FROM blogs WHERE blogs.title=U&'d!0061t!+000061' UESCAPE '!' \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_1logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_1logical.txt deleted file mode 100644 index 7476bebad..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_1logical.txt +++ /dev/null @@ -1,3 +0,0 @@ -processor : 0 -model name : AMD Duron(tm) processor -cache size : 64 KB diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_2logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_2logical.txt deleted file mode 100644 index fab5fa56a..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_1core_2logical.txt +++ /dev/null @@ -1,14 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_2core_2logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_2core_2logical.txt deleted file mode 100644 index 0281fcf51..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_2core_2logical.txt +++ /dev/null @@ -1,14 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_4core_4logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_4core_4logical.txt deleted file mode 100644 index 0a3bf3781..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/1pack_4core_4logical.txt +++ /dev/null @@ -1,28 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_12core_24logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_12core_24logical.txt deleted file mode 100644 index 4177c0361..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_12core_24logical.txt +++ /dev/null @@ -1,575 +0,0 @@ -processor : 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] diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_20core_40logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_20core_40logical.txt deleted file mode 100644 index 709087d31..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_20core_40logical.txt +++ /dev/null @@ -1,999 +0,0 @@ -processor : 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: diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_2logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_2logical.txt deleted file mode 100644 index f75e9fac9..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_2logical.txt +++ /dev/null @@ -1,51 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_4logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_4logical.txt deleted file mode 100644 index cedcb7aa4..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_2core_4logical.txt +++ /dev/null @@ -1,28 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_4core_4logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_4core_4logical.txt deleted file mode 100644 index 6e6f418b9..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/2pack_4core_4logical.txt +++ /dev/null @@ -1,28 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/4pack_4core_4logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/4pack_4core_4logical.txt deleted file mode 100644 index c7a816e12..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/4pack_4core_4logical.txt +++ /dev/null @@ -1,103 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/8pack_8core_8logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/8pack_8core_8logical.txt deleted file mode 100644 index afdb880e5..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/8pack_8core_8logical.txt +++ /dev/null @@ -1,199 +0,0 @@ -processor : 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: diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/README.md b/internal/crossagent/cross_agent_tests/proc_cpuinfo/README.md deleted file mode 100644 index 04997821a..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/README.md +++ /dev/null @@ -1,28 +0,0 @@ -These 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. diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/Xpack_Xcore_2logical.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/Xpack_Xcore_2logical.txt deleted file mode 100644 index 9a8e9dab5..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/Xpack_Xcore_2logical.txt +++ /dev/null @@ -1,43 +0,0 @@ -processor : 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 diff --git a/internal/crossagent/cross_agent_tests/proc_cpuinfo/malformed_file.txt b/internal/crossagent/cross_agent_tests/proc_cpuinfo/malformed_file.txt deleted file mode 100644 index 5dfa01375..000000000 --- a/internal/crossagent/cross_agent_tests/proc_cpuinfo/malformed_file.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a random text file that does NOT adhere to the /proc/cpuinfo format. -xxxYYYZZz - diff --git a/internal/crossagent/cross_agent_tests/proc_meminfo/README.md b/internal/crossagent/cross_agent_tests/proc_meminfo/README.md deleted file mode 100644 index 29e3f6835..000000000 --- a/internal/crossagent/cross_agent_tests/proc_meminfo/README.md +++ /dev/null @@ -1,7 +0,0 @@ -These 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. diff --git a/internal/crossagent/cross_agent_tests/proc_meminfo/meminfo_4096MB.txt b/internal/crossagent/cross_agent_tests/proc_meminfo/meminfo_4096MB.txt deleted file mode 100644 index f9f10b250..000000000 --- a/internal/crossagent/cross_agent_tests/proc_meminfo/meminfo_4096MB.txt +++ /dev/null @@ -1,47 +0,0 @@ -MemTotal: 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 - diff --git a/internal/crossagent/cross_agent_tests/rules.json b/internal/crossagent/cross_agent_tests/rules.json deleted file mode 100644 index 588201628..000000000 --- a/internal/crossagent/cross_agent_tests/rules.json +++ /dev/null @@ -1,162 +0,0 @@ -[ - { - "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"} - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/rum_client_config.json b/internal/crossagent/cross_agent_tests/rum_client_config.json deleted file mode 100644 index 8f6e7cbbb..000000000 --- a/internal/crossagent/cross_agent_tests/rum_client_config.json +++ /dev/null @@ -1,91 +0,0 @@ -[ - { - "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":"" - } - } -] diff --git a/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/close-body-in-comment.html b/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/close-body-in-comment.html deleted file mode 100644 index 3252494c2..000000000 --- a/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/close-body-in-comment.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Comment contains a close body tag - - -

The quick brown fox jumps over the lazy dog.

- - EXPECTED_RUM_FOOTER_LOCATION - diff --git a/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/dynamic-iframe.html b/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/dynamic-iframe.html deleted file mode 100644 index 18b82ff80..000000000 --- a/internal/crossagent/cross_agent_tests/rum_footer_insertion_location/dynamic-iframe.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - Dynamic iframe Generation - - -

The quick brown fox jumps over the lazy dog.

- - - EXPECTED_RUM_FOOTER_LOCATION - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/basic.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/basic.html deleted file mode 100644 index 4afa155ee..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/basic.html +++ /dev/null @@ -1,10 +0,0 @@ - - EXPECTED_RUM_LOADER_LOCATION - im a title - - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/body_with_attributes.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/body_with_attributes.html deleted file mode 100644 index 5442cdbec..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/body_with_attributes.html +++ /dev/null @@ -1,3 +0,0 @@ -EXPECTED_RUM_LOADER_LOCATION - This isn't great HTML but it's what we've got. - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag.html deleted file mode 100644 index b050317ba..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - - EXPECTED_RUM_LOADER_LOCATION - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_after_x_ua_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_after_x_ua_tag.html deleted file mode 100644 index 7cbc188a2..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_after_x_ua_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_before_x_ua_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_before_x_ua_tag.html deleted file mode 100644 index a8f5fcd23..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_before_x_ua_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_with_spaces.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_with_spaces.html deleted file mode 100644 index 64ba08f87..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/charset_tag_with_spaces.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - - EXPECTED_RUM_LOADER_LOCATION - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments1.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments1.html deleted file mode 100644 index 08c1ada77..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments1.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments2.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments2.html deleted file mode 100644 index 01d895eb4..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/comments2.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag.html deleted file mode 100644 index 3543dab41..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - - EXPECTED_RUM_LOADER_LOCATION - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_after_x_ua_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_after_x_ua_tag.html deleted file mode 100644 index 1f1f91ee3..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_after_x_ua_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_before_x_ua_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_before_x_ua_tag.html deleted file mode 100644 index ccbed78e5..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/content_type_charset_tag_before_x_ua_tag.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes1.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes1.html deleted file mode 100644 index 076cc62a6..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes1.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - OPT® - - - - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes2.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes2.html deleted file mode 100644 index 06fd00bcd..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes2.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes_mismatch.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes_mismatch.html deleted file mode 100644 index 4c48890db..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_quotes_mismatch.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - ication" content="xxxx" /> - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes1.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes1.html deleted file mode 100644 index faa63f840..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes1.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes_mismatch.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes_mismatch.html deleted file mode 100644 index 7109cc8f7..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/gt_in_single_quotes_mismatch.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - .01' content='yyyy\' /> - - - - - - OPT® - - - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/head_with_attributes.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/head_with_attributes.html deleted file mode 100644 index 07f73ada2..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/head_with_attributes.html +++ /dev/null @@ -1,10 +0,0 @@ - - EXPECTED_RUM_LOADER_LOCATION - im a title - - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/incomplete_non_meta_tags.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/incomplete_non_meta_tags.html deleted file mode 100644 index 3003f0582..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/incomplete_non_meta_tags.html +++ /dev/null @@ -1,10 +0,0 @@ - - EXPECTED_RUM_LOADER_LOCATION - - - -EXPECTED_RUM_LOADER_LOCATION - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_header.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_header.html deleted file mode 100644 index ee8e6ce07..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_header.html +++ /dev/null @@ -1,7 +0,0 @@ - - - EXPECTED_RUM_LOADER_LOCATION - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_html_and_no_header.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_html_and_no_header.html deleted file mode 100644 index 8e01bd2c8..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_html_and_no_header.html +++ /dev/null @@ -1,3 +0,0 @@ -EXPECTED_RUM_LOADER_LOCATION - This isn't great HTML but it's what we've got. - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_start_header.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_start_header.html deleted file mode 100644 index 4525b759a..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/no_start_header.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - EXPECTED_RUM_LOADER_LOCATION - Cribbed from the Java agent - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script1.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script1.html deleted file mode 100644 index 20b2f6b66..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script1.html +++ /dev/null @@ -1,19 +0,0 @@ - - - EXPECTED_RUM_LOADER_LOCATION - - Castor - - - - - - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script2.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script2.html deleted file mode 100644 index 90b01c5e5..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/script2.html +++ /dev/null @@ -1,17 +0,0 @@ - - - EXPECTED_RUM_LOADER_LOCATION - Castor - - - - - - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag.html deleted file mode 100644 index 950d9a73e..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag.html +++ /dev/null @@ -1,10 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiline.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiline.html deleted file mode 100644 index a801f6ca9..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiline.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiple_tags.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiple_tags.html deleted file mode 100644 index d43fec3d2..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_multiple_tags.html +++ /dev/null @@ -1,12 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_spaces_around_equals.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_spaces_around_equals.html deleted file mode 100644 index dd006e746..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_spaces_around_equals.html +++ /dev/null @@ -1,10 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_others.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_others.html deleted file mode 100644 index 57674c5f8..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_others.html +++ /dev/null @@ -1,11 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_spaces.html b/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_spaces.html deleted file mode 100644 index 215ee13b5..000000000 --- a/internal/crossagent/cross_agent_tests/rum_loader_insertion_location/x_ua_meta_tag_with_spaces.html +++ /dev/null @@ -1,10 +0,0 @@ - - - im a title - EXPECTED_RUM_LOADER_LOCATION - - - im some body text - diff --git a/internal/crossagent/cross_agent_tests/sql_obfuscation/README.md b/internal/crossagent/cross_agent_tests/sql_obfuscation/README.md deleted file mode 100644 index ab51e95ce..000000000 --- a/internal/crossagent/cross_agent_tests/sql_obfuscation/README.md +++ /dev/null @@ -1,36 +0,0 @@ -These 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) diff --git a/internal/crossagent/cross_agent_tests/sql_obfuscation/sql_obfuscation.json b/internal/crossagent/cross_agent_tests/sql_obfuscation/sql_obfuscation.json deleted file mode 100644 index 69dd2b8c4..000000000 --- a/internal/crossagent/cross_agent_tests/sql_obfuscation/sql_obfuscation.json +++ /dev/null @@ -1,650 +0,0 @@ -[ - { - "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" - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/sql_parsing.json b/internal/crossagent/cross_agent_tests/sql_parsing.json deleted file mode 100644 index 5590f2f93..000000000 --- a/internal/crossagent/cross_agent_tests/sql_parsing.json +++ /dev/null @@ -1,67 +0,0 @@ -[ - {"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} -] diff --git a/internal/crossagent/cross_agent_tests/synthetics/README.md b/internal/crossagent/cross_agent_tests/synthetics/README.md deleted file mode 100644 index 61acbc83c..000000000 --- a/internal/crossagent/cross_agent_tests/synthetics/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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. | diff --git a/internal/crossagent/cross_agent_tests/synthetics/synthetics.json b/internal/crossagent/cross_agent_tests/synthetics/synthetics.json deleted file mode 100644 index 6a429fe42..000000000 --- a/internal/crossagent/cross_agent_tests/synthetics/synthetics.json +++ /dev/null @@ -1,317 +0,0 @@ -[ - { - "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" - ] - } - } -] diff --git a/internal/crossagent/cross_agent_tests/transaction_segment_terms.json b/internal/crossagent/cross_agent_tests/transaction_segment_terms.json deleted file mode 100644 index 3caf5d097..000000000 --- a/internal/crossagent/cross_agent_tests/transaction_segment_terms.json +++ /dev/null @@ -1,389 +0,0 @@ -[ - { - "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/*" - } - ] - } -] diff --git a/internal/crossagent/cross_agent_tests/url_clean.json b/internal/crossagent/cross_agent_tests/url_clean.json deleted file mode 100644 index 3f370c379..000000000 --- a/internal/crossagent/cross_agent_tests/url_clean.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - {"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"} -] diff --git a/internal/crossagent/cross_agent_tests/url_domain_extraction.json b/internal/crossagent/cross_agent_tests/url_domain_extraction.json deleted file mode 100644 index 9ac22a33c..000000000 --- a/internal/crossagent/cross_agent_tests/url_domain_extraction.json +++ /dev/null @@ -1,35 +0,0 @@ -[ - {"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"} -] diff --git a/internal/crossagent/cross_agent_tests/utilization/README.md b/internal/crossagent/cross_agent_tests/utilization/README.md deleted file mode 100644 index a51057036..000000000 --- a/internal/crossagent/cross_agent_tests/utilization/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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. diff --git a/internal/crossagent/cross_agent_tests/utilization/boot_id.json b/internal/crossagent/cross_agent_tests/utilization/boot_id.json deleted file mode 100644 index 765a5dcf1..000000000 --- a/internal/crossagent/cross_agent_tests/utilization/boot_id.json +++ /dev/null @@ -1,109 +0,0 @@ -[ - { - "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 - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/utilization/utilization_json.json b/internal/crossagent/cross_agent_tests/utilization/utilization_json.json deleted file mode 100644 index a5ed101bc..000000000 --- a/internal/crossagent/cross_agent_tests/utilization/utilization_json.json +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/README.md b/internal/crossagent/cross_agent_tests/utilization_vendor_specific/README.md deleted file mode 100644 index d43976338..000000000 --- a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 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. \ No newline at end of file diff --git a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/aws.json b/internal/crossagent/cross_agent_tests/utilization_vendor_specific/aws.json deleted file mode 100644 index 97bc04771..000000000 --- a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/aws.json +++ /dev/null @@ -1,233 +0,0 @@ -[ - { - "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" - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/azure.json b/internal/crossagent/cross_agent_tests/utilization_vendor_specific/azure.json deleted file mode 100644 index 0d32c5c67..000000000 --- a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/azure.json +++ /dev/null @@ -1,287 +0,0 @@ -[ - { - "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" - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/gcp.json b/internal/crossagent/cross_agent_tests/utilization_vendor_specific/gcp.json deleted file mode 100644 index 9912790c0..000000000 --- a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/gcp.json +++ /dev/null @@ -1,288 +0,0 @@ -[ - { - "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" - } - } - } -] diff --git a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/pcf.json b/internal/crossagent/cross_agent_tests/utilization_vendor_specific/pcf.json deleted file mode 100644 index 323dcbab7..000000000 --- a/internal/crossagent/cross_agent_tests/utilization_vendor_specific/pcf.json +++ /dev/null @@ -1,281 +0,0 @@ -[ - { - "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" - } - } - } -] diff --git a/internal/crossagent/crossagent.go b/internal/crossagent/crossagent.go deleted file mode 100644 index e256f6908..000000000 --- a/internal/crossagent/crossagent.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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)) -} - -// 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 -} diff --git a/internal/custom_event.go b/internal/custom_event.go deleted file mode 100644 index 876695054..000000000 --- a/internal/custom_event.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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.floatField("timestamp", timeToFloatSeconds(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 -} - -// MergeIntoHarvest implements Harvestable. -func (e *CustomEvent) MergeIntoHarvest(h *Harvest) { - h.CustomEvents.Add(e) -} diff --git a/internal/custom_event_test.go b/internal/custom_event_test.go deleted file mode 100644 index 760ebe96e..000000000 --- a/internal/custom_event_test.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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":1.41713646e+09},{"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":1.41713646e+09},{},{}]` { - 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":1.41713646e+09},{},{}]` { - 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":1.41713646e+09},{"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":1.41713646e+09},{"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":1.41713646e+09},{"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)) - } -} diff --git a/internal/custom_events.go b/internal/custom_events.go deleted file mode 100644 index f874f743a..000000000 --- a/internal/custom_events.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 -} diff --git a/internal/custom_metric.go b/internal/custom_metric.go deleted file mode 100644 index 835805f97..000000000 --- a/internal/custom_metric.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -// CustomMetric is a custom metric. -type CustomMetric struct { - RawInputName string - Value float64 -} - -// MergeIntoHarvest implements Harvestable. -func (m CustomMetric) MergeIntoHarvest(h *Harvest) { - h.Metrics.addValue(customMetric(m.RawInputName), "", m.Value, unforced) -} diff --git a/internal/distributed_tracing.go b/internal/distributed_tracing.go deleted file mode 100644 index f1d5b364f..000000000 --- a/internal/distributed_tracing.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "time" -) - -type distTraceVersion [2]int - -func (v distTraceVersion) major() int { return v[0] } -func (v distTraceVersion) minor() int { return v[1] } - -const ( - // CallerType is the Type field's value for outbound payloads. - CallerType = "App" -) - -var ( - currentDistTraceVersion = distTraceVersion([2]int{0 /* Major */, 1 /* Minor */}) - callerUnknown = payloadCaller{Type: "Unknown", App: "Unknown", Account: "Unknown", TransportType: "Unknown"} -) - -// 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())) -} - -func (tm timestampMillis) Time() time.Time { return time.Time(tm) } -func (tm *timestampMillis) Set(t time.Time) { *tm = timestampMillis(t) } - -// Payload is the distributed tracing payload. -type Payload struct { - payloadCaller - TransactionID string `json:"tx,omitempty"` - ID string `json:"id,omitempty"` - TracedID string `json:"tr"` - Priority Priority `json:"pr"` - Sampled *bool `json:"sa"` - Timestamp timestampMillis `json:"ti"` - TransportDuration time.Duration `json:"-"` -} - -type payloadCaller struct { - TransportType string `json:"-"` - Type string `json:"ty"` - App string `json:"ap"` - Account string `json:"ac"` - TrustedAccountKey string `json:"tk,omitempty"` -} - -// IsValid validates the payload data by looking for missing fields. -// Returns an error if there's a problem, nil if everything's fine -func (p Payload) IsValid() 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 ErrPayloadMissingField{message: "missing both guid/id and TransactionId/tx"} - } - - if "" == p.Type { - return ErrPayloadMissingField{message: "missing Type/ty"} - } - - if "" == p.Account { - return ErrPayloadMissingField{message: "missing Account/ac"} - } - - if "" == p.App { - return ErrPayloadMissingField{message: "missing App/ap"} - } - - if "" == p.TracedID { - return ErrPayloadMissingField{message: "missing TracedID/tr"} - } - - if p.Timestamp.Time().IsZero() || 0 == p.Timestamp.Time().Unix() { - return ErrPayloadMissingField{message: "missing Timestamp/ti"} - } - - return nil -} - -func (p Payload) text(v distTraceVersion) []byte { - js, _ := json.Marshal(struct { - Version distTraceVersion `json:"v"` - Data Payload `json:"d"` - }{ - Version: v, - Data: p, - }) - return js -} - -// Text implements newrelic.DistributedTracePayload. -func (p Payload) Text() string { - t := p.text(currentDistTraceVersion) - return string(t) -} - -// HTTPSafe implements newrelic.DistributedTracePayload. -func (p Payload) HTTPSafe() string { - t := p.text(currentDistTraceVersion) - return base64.StdEncoding.EncodeToString(t) -} - -// 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 = &sampled -} - -// ErrPayloadParse indicates that the payload was malformed. -type ErrPayloadParse struct{ err error } - -func (e ErrPayloadParse) Error() string { - return fmt.Sprintf("unable to parse inbound payload: %s", e.err.Error()) -} - -// ErrPayloadMissingField indicates there's a required field that's missing -type ErrPayloadMissingField struct{ message string } - -func (e ErrPayloadMissingField) Error() string { - return fmt.Sprintf("payload is missing required fields: %s", e.message) -} - -// ErrUnsupportedPayloadVersion indicates that the major version number is -// unknown. -type ErrUnsupportedPayloadVersion struct{ version int } - -func (e ErrUnsupportedPayloadVersion) Error() string { - return fmt.Sprintf("unsupported major version number %d", e.version) -} - -// AcceptPayload parses the inbound distributed tracing payload. -func AcceptPayload(p interface{}) (*Payload, error) { - var payload Payload - if byteSlice, ok := p.([]byte); ok { - p = string(byteSlice) - } - switch v := p.(type) { - case string: - if "" == v { - return nil, nil - } - var decoded []byte - if '{' == v[0] { - decoded = []byte(v) - } else { - var err error - decoded, err = base64.StdEncoding.DecodeString(v) - if nil != err { - return nil, ErrPayloadParse{err: err} - } - } - envelope := struct { - Version distTraceVersion `json:"v"` - Data json.RawMessage `json:"d"` - }{} - if err := json.Unmarshal(decoded, &envelope); nil != err { - return nil, ErrPayloadParse{err: err} - } - - if 0 == envelope.Version.major() && 0 == envelope.Version.minor() { - return nil, ErrPayloadMissingField{message: "missing v"} - } - - if envelope.Version.major() > currentDistTraceVersion.major() { - return nil, ErrUnsupportedPayloadVersion{ - version: envelope.Version.major(), - } - } - if err := json.Unmarshal(envelope.Data, &payload); nil != err { - return nil, ErrPayloadParse{err: err} - } - case Payload: - payload = v - default: - // Could be a shim payload (if the app is not yet connected). - return nil, nil - } - // Ensure that we don't have a reference to the input payload: we don't - // want to change it, it could be used multiple times. - alloc := new(Payload) - *alloc = payload - - return alloc, nil -} diff --git a/internal/distributed_tracing_test.go b/internal/distributed_tracing_test.go deleted file mode 100644 index 956f9a3d6..000000000 --- a/internal/distributed_tracing_test.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "testing" - "time" -) - -var ( - samplePayload = Payload{ - payloadCaller: payloadCaller{ - Type: CallerType, - Account: "123", - App: "456", - }, - ID: "myid", - TracedID: "mytrip", - Priority: 0.12345, - Timestamp: timestampMillis(time.Now()), - } -) - -func TestPayloadRaw(t *testing.T) { - out, err := AcceptPayload(samplePayload) - if err != nil || out == nil { - t.Fatal(err, out) - } - if samplePayload != *out { - t.Fatal(samplePayload, out) - } -} - -func TestPayloadNil(t *testing.T) { - out, err := AcceptPayload(nil) - if err != nil || out != nil { - t.Fatal(err, out) - } -} - -func TestPayloadText(t *testing.T) { - out, err := AcceptPayload(samplePayload.Text()) - if err != nil || out == nil { - t.Fatal(err, out) - } - out.Timestamp = samplePayload.Timestamp // account for timezone differences - if samplePayload != *out { - t.Fatal(samplePayload, out) - } -} - -func TestPayloadTextByteSlice(t *testing.T) { - out, err := AcceptPayload([]byte(samplePayload.Text())) - if err != nil || out == nil { - t.Fatal(err, out) - } - out.Timestamp = samplePayload.Timestamp // account for timezone differences - if samplePayload != *out { - t.Fatal(samplePayload, out) - } -} - -func TestPayloadHTTPSafe(t *testing.T) { - out, err := AcceptPayload(samplePayload.HTTPSafe()) - if err != nil || nil == out { - t.Fatal(err, out) - } - out.Timestamp = samplePayload.Timestamp // account for timezone differences - if samplePayload != *out { - t.Fatal(samplePayload, out) - } -} - -func TestPayloadHTTPSafeByteSlice(t *testing.T) { - out, err := AcceptPayload([]byte(samplePayload.HTTPSafe())) - if err != nil || nil == out { - t.Fatal(err, out) - } - out.Timestamp = samplePayload.Timestamp // account for timezone differences - if samplePayload != *out { - t.Fatal(samplePayload, out) - } -} - -func TestPayloadInvalidBase64(t *testing.T) { - out, err := AcceptPayload("======") - if _, ok := err.(ErrPayloadParse); !ok { - t.Fatal(err) - } - if nil != out { - t.Fatal(out) - } -} - -func TestPayloadEmptyString(t *testing.T) { - out, err := AcceptPayload("") - if err != nil { - t.Fatal(err) - } - if nil != out { - t.Fatal(out) - } -} - -func TestPayloadUnexpectedType(t *testing.T) { - out, err := AcceptPayload(1) - if err != nil { - t.Fatal(err) - } - if nil != out { - t.Fatal(out) - } -} - -func TestPayloadBadVersion(t *testing.T) { - futuristicVersion := distTraceVersion([2]int{ - currentDistTraceVersion[0] + 1, - currentDistTraceVersion[1] + 1, - }) - out, err := AcceptPayload(samplePayload.text(futuristicVersion)) - if _, ok := err.(ErrUnsupportedPayloadVersion); !ok { - t.Fatal(err) - } - if out != nil { - t.Fatal(out) - } -} - -func TestPayloadBadEnvelope(t *testing.T) { - out, err := AcceptPayload("{") - if _, ok := err.(ErrPayloadParse); !ok { - t.Fatal(err) - } - if out != nil { - t.Fatal(out) - } -} - -func TestPayloadBadPayload(t *testing.T) { - var envelope map[string]interface{} - if err := json.Unmarshal([]byte(samplePayload.Text()), &envelope); nil != err { - t.Fatal(err) - } - envelope["d"] = "123" - payload, err := json.Marshal(envelope) - if nil != err { - t.Fatal(err) - } - out, err := AcceptPayload(payload) - if _, ok := err.(ErrPayloadParse); !ok { - t.Fatal(err) - } - if out != nil { - t.Fatal(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.Text() - } -} - -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.IsValid(); 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.IsValid(); 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.IsValid(); 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.IsValid(); 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.IsValid(); 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.IsValid(); 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.IsValid(); err == nil { - t.Log("Expected error from missing Timestamp (ti)") - t.Fail() - } -} diff --git a/internal/environment.go b/internal/environment.go deleted file mode 100644 index d6f56ccfa..000000000 --- a/internal/environment.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "reflect" - "runtime" -) - -// Environment describes the application's environment. -type Environment struct { - Compiler string `env:"runtime.Compiler"` - GOARCH string `env:"runtime.GOARCH"` - GOOS string `env:"runtime.GOOS"` - Version string `env:"runtime.Version"` - NumCPU int `env:"runtime.NumCPU"` -} - -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() Environment { - return Environment{ - Compiler: runtime.Compiler, - GOARCH: runtime.GOARCH, - GOOS: runtime.GOOS, - Version: runtime.Version(), - NumCPU: runtime.NumCPU(), - } -} - -// 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) -} diff --git a/internal/environment_test.go b/internal/environment_test.go deleted file mode 100644 index 5ca152da0..000000000 --- a/internal/environment_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "runtime" - "testing" -) - -func TestMarshalEnvironment(t *testing.T) { - js, err := json.Marshal(&SampleEnvironment) - if nil != err { - t.Fatal(err) - } - expect := CompactJSONString(`[ - ["runtime.Compiler","comp"], - ["runtime.GOARCH","arch"], - ["runtime.GOOS","goos"], - ["runtime.Version","vers"], - ["runtime.NumCPU",8]]`) - if string(js) != expect { - t.Fatal(string(js)) - } -} - -func TestEnvironmentFields(t *testing.T) { - env := NewEnvironment() - 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()) - } -} diff --git a/internal/error_events.go b/internal/error_events.go deleted file mode 100644 index 9ad1efe25..000000000 --- a/internal/error_events.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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.floatField("timestamp", timeToFloatSeconds(e.When)) - w.stringField("transactionName", e.FinalName) - - sharedTransactionIntrinsics(&e.TxnEvent, &w) - sharedBetterCATIntrinsics(&e.TxnEvent, &w) - - buf.WriteByte('}') - buf.WriteByte(',') - userAttributesJSON(e.Attrs, buf, destError, e.ErrorData.ExtraAttributes) - buf.WriteByte(',') - 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, priority Priority) { - events.addEvent(analyticsEvent{priority, 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 -} diff --git a/internal/error_events_test.go b/internal/error_events_test.go deleted file mode 100644 index 5315a65eb..000000000 --- a/internal/error_events_test.go +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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, - ID: "txn-guid-id", - }, - }, - }, `[ - { - "type":"TransactionError", - "error.class":"*errors.errorString", - "error.message":"hello", - "timestamp":1.41713646e+09, - "transactionName":"myName", - "duration":3, - "guid":"txn-guid-id", - "traceId":"txn-guid-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":1.41713646e+09, - "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 := sampleAttributeConfigInput - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, "zap") - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, AttributeHostDisplayName.name()) - 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, - ID: "txn-guid-id", - }, - }, - }, `[ - { - "type":"TransactionError", - "error.class":"*errors.errorString", - "error.message":"hello", - "timestamp":1.41713646e+09, - "transactionName":"myName", - "duration":3, - "guid":"txn-guid-id", - "traceId":"txn-guid-id", - "priority":0.500000, - "sampled":false - }, - { - "zip":456 - }, - { - "request.method":"GET" - } - ]`) -} - -func TestErrorEventAttributesOldCAT(t *testing.T) { - aci := sampleAttributeConfigInput - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, "zap") - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, AttributeHostDisplayName.name()) - 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":1.41713646e+09, - "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.Inbound = &Payload{ - payloadCaller: payloadCaller{ - TransportType: "HTTP", - Type: "Browser", - App: "caller-app", - Account: "caller-account", - }, - ID: "caller-id", - TransactionID: "caller-parent-id", - TracedID: "trip-id", - TransportDuration: 2 * time.Second, - } - - testErrorEventJSON(t, &ErrorEvent{ - ErrorData: sampleErrorData, - TxnEvent: e, - }, `[ - { - "type":"TransactionError", - "error.class":"*errors.errorString", - "error.message":"hello", - "timestamp":1.41713646e+09, - "transactionName":"myName", - "duration":3, - "parent.type": "Browser", - "parent.app": "caller-app", - "parent.account": "caller-account", - "parent.transportType": "HTTP", - "parent.transportDuration": 2, - "guid":"", - "traceId":"trip-id", - "priority":0.000000, - "sampled":false - }, - {}, - {} - ]`) -} diff --git a/internal/errors.go b/internal/errors.go deleted file mode 100644 index cbee8fa9d..000000000 --- a/internal/errors.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/newrelic/go-agent/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 - ExtraAttributes map[string]interface{} - Msg string - Klass string -} - -// 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) - if nil != h.Stack { - buf.WriteByte(',') - buf.WriteString(`"stack_trace"`) - buf.WriteByte(':') - h.Stack.WriteJSON(buf) - } - buf.WriteByte('}') - - 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) { - for _, e := range errs { - if len(*errors) == cap(*errors) { - return - } - *errors = append(*errors, &tracedError{ - TxnEvent: txnEvent, - ErrorData: *e, - }) - } -} - -func (errors harvestErrors) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { - if 0 == len(errors) { - 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 -} diff --git a/internal/errors_test.go b/internal/errors_test.go deleted file mode 100644 index 2009afddd..000000000 --- a/internal/errors_test.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 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, - BetterCAT: BetterCAT{ - Enabled: true, - ID: "txn-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":"txn-id", - "priority":0.500000, - "sampled":false - }, - "stack_trace":[] - } - ]` - 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, - }, - } - 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":[] - } - ]` - testExpectedJSON(t, expect, string(js)) -} - -func TestErrorTraceAttributes(t *testing.T) { - aci := sampleAttributeConfigInput - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, "zap") - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, AttributeHostDisplayName.name()) - 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, - BetterCAT: BetterCAT{ - Enabled: true, - ID: "txn-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":{"request.uri":"my_request_uri"}, - "userAttributes":{"zip":456}, - "intrinsics":{ - "totalTime":2, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - } - } - ]` - testExpectedJSON(t, expect, string(js)) -} - -func TestErrorTraceAttributesOldCAT(t *testing.T) { - aci := sampleAttributeConfigInput - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, "zap") - aci.ErrorCollector.Exclude = append(aci.ErrorCollector.Exclude, AttributeHostDisplayName.name()) - 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, - 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 - } - } - ]` - 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, - BetterCAT: BetterCAT{ - Enabled: true, - ID: "txn-id", - Priority: 0.5, - }, - TotalTime: 2 * time.Second, - }) - 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":"txn-id", - "priority":0.500000, - "sampled":false - } - } - ], - [ - 1.41713646e+12, - "txnName", - "Bad Request", - "400", - { - "agentAttributes":{}, - "userAttributes":{}, - "intrinsics":{ - "totalTime":2, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - } - } - ], - [ - 1.41713646e+12, - "txnName", - "oh no panic", - "panic", - { - "agentAttributes":{}, - "userAttributes":{}, - "intrinsics":{ - "totalTime":2, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - } - } - ], - [ - 1.41713646e+12, - "txnName", - "123", - "panic", - { - "agentAttributes":{}, - "userAttributes":{}, - "intrinsics":{ - "totalTime":2, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - } - } - ] - ] -]`) - if string(js) != expect { - t.Error(string(js), 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(sampleAttributeConfigInput, 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, - }) - - 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) - } - } -} diff --git a/internal/expect.go b/internal/expect.go deleted file mode 100644 index d97016fc4..000000000 --- a/internal/expect.go +++ /dev/null @@ -1,598 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "fmt" - "runtime" - - "time" -) - -var ( - // Unfortunately, the resolution of time.Now() on Windows is coarse: Two - // sequential calls to time.Now() may return the same value, and tests - // which expect non-zero durations may fail. To avoid adding sleep - // statements or mocking time.Now(), those tests are skipped on Windows. - doDurationTests = runtime.GOOS != `windows` -) - -// Validator is used for testing. -type Validator interface { - Error(...interface{}) -} - -func validateStringField(v 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 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 Validator, field interface{}) Validator { - return addValidatorField{ - field: field, - original: v, - } -} - -// 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 - UserAttributes map[string]interface{} - AgentAttributes map[string]interface{} -} - -func uniquePointer() *struct{} { - s := struct{}{} - return &s -} - -var ( - // MatchAnything is for use when matching attributes. - MatchAnything = uniquePointer() -) - -// 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 { - 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 -} - -// ExpectTxnMetrics tests that the app contains metrics for a transaction. -func ExpectTxnMetrics(t Validator, mt *metricTable, want WantTxn) { - var metrics []WantMetric - var scope string - var allWebOther string - if want.IsWeb { - scope = "WebTransaction/Go/" + want.Name - allWebOther = "allWeb" - metrics = []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}, - } - } else { - scope = "OtherTransaction/Go/" + want.Name - allWebOther = "allOther" - metrics = []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.NumErrors > 0 { - data := []float64{float64(want.NumErrors), 0, 0, 0, 0, 0} - metrics = append(metrics, []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) -} - -// Expect exposes methods that allow for testing whether the correct data was -// captured. -type Expect interface { - ExpectCustomEvents(t Validator, want []WantEvent) - 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) -} - -func expectMetricField(t Validator, id metricID, v1, v2 float64, fieldName string) { - if v1 != v2 { - t.Error("metric fields do not match", id, v1, v2, fieldName) - } -} - -// ExpectMetricsPresent allows testing of metrics without requiring an exact match -func ExpectMetricsPresent(t Validator, mt *metricTable, expect []WantMetric) { - expectMetrics(t, mt, expect, false) -} - -// ExpectMetrics allows testing of metrics. It passes if mt exactly matches expect. -func ExpectMetrics(t Validator, mt *metricTable, expect []WantMetric) { - expectMetrics(t, mt, expect, true) -} - -func expectMetrics(t Validator, mt *metricTable, expect []WantMetric, exactMatch bool) { - if exactMatch { - if len(mt.metrics) != len(expect) { - t.Error("metric counts do not match expectations", len(mt.metrics), len(expect)) - } - } - 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("unable to find metric", 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 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, val := range expect { - found, ok := exists[key] - if !ok { - v.Error("expected attribute not found: ", key) - continue - } - if val == MatchAnything { - continue - } - v1 := fmt.Sprint(found) - v2 := fmt.Sprint(val) - if v1 != v2 { - v.Error("value difference", fmt.Sprintf("key=%s", key), v1, v2) - } - } - 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 Validator, cs *customEvents, expect []WantEvent) { - expectEvents(v, cs.analyticsEvents, expect, nil) -} - -func expectEvent(v Validator, e json.Marshaler, expect WantEvent) { - js, err := e.MarshalJSON() - if nil != err { - v.Error("unable to marshal event", err) - return - } - var event []map[string]interface{} - err = json.Unmarshal(js, &event) - if nil != err { - v.Error("unable to parse event json", err) - 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 Validator, events *analyticsEvents, expect []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 Validator, events *errorEvents, expect []WantEvent) { - expectEvents(v, events.analyticsEvents, expect, map[string]interface{}{ - // The following intrinsics should always be present in - // error events: - "type": "TransactionError", - "timestamp": MatchAnything, - "duration": MatchAnything, - }) -} - -// ExpectSpanEvents allows testing of span events. It passes if events exactly matches expect. -func ExpectSpanEvents(v Validator, events *spanEvents, expect []WantEvent) { - expectEvents(v, events.analyticsEvents, expect, map[string]interface{}{ - // The following intrinsics should always be present in - // span events: - "type": "Span", - "timestamp": MatchAnything, - "duration": MatchAnything, - "traceId": MatchAnything, - "guid": MatchAnything, - "transactionId": MatchAnything, - // All span events are currently sampled. - "sampled": true, - "priority": MatchAnything, - }) -} - -// ExpectTxnEvents allows testing of txn events. -func ExpectTxnEvents(v Validator, events *txnEvents, expect []WantEvent) { - expectEvents(v, events.analyticsEvents, expect, map[string]interface{}{ - // The following intrinsics should always be present in - // txn events: - "type": "Transaction", - "timestamp": MatchAnything, - "duration": MatchAnything, - "totalTime": MatchAnything, - "error": MatchAnything, - }) -} - -func expectError(v Validator, err *tracedError, expect 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 Validator, errors harvestErrors, expect []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 Validator, nodeObj interface{}, expect 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 Validator, got interface{}, expect 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 doDurationTests && 0 == duration { - v.Error("zero trace 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 Validator, traces *harvestTraces, want []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 Validator, slowQuery *slowQuery, want 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", slowQuery.DatastoreMetric, want.MetricName) - validateStringField(t, "Query", slowQuery.ParameterizedQuery, want.Query) - validateStringField(t, "TxnEvent.FinalName", slowQuery.TxnEvent.FinalName, want.TxnName) - validateStringField(t, "request.uri", uri, want.TxnURL) - validateStringField(t, "DatabaseName", slowQuery.DatabaseName, want.DatabaseName) - validateStringField(t, "Host", slowQuery.Host, want.Host) - validateStringField(t, "PortPathOrID", slowQuery.PortPathOrID, want.PortPathOrID) - expectAttributes(t, map[string]interface{}(slowQuery.QueryParameters), want.Params) -} - -// ExpectSlowQueries allows testing of slow queries. -func ExpectSlowQueries(t Validator, slowQueries *slowQueries, want []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) - } -} diff --git a/internal/harvest.go b/internal/harvest.go deleted file mode 100644 index bfdb3b104..000000000 --- a/internal/harvest.go +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "strings" - "sync" - "time" -) - -// 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 is the Metrics Traces type - HarvestMetricsTraces HarvestTypes = 1 << iota - // HarvestSpanEvents is the Span Event type - HarvestSpanEvents - // HarvestCustomEvents is the Custom Event type - HarvestCustomEvents - // HarvestTxnEvents is the Transaction Event type - HarvestTxnEvents - // HarvestErrorEvents is the Error Event type - HarvestErrorEvents -) - -const ( - // HarvestTypesEvents includes all Event types - HarvestTypesEvents = HarvestSpanEvents | HarvestCustomEvents | HarvestTxnEvents | HarvestErrorEvents - // 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 - 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&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.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 -} - -// MaxTxnEventer returns the maximum number of Transaction Events that should be reported per period -type MaxTxnEventer interface { - MaxTxnEvents() int -} - -// HarvestConfigurer contains information about the configured number of various -// types of events as well as the Faster Event Harvest report period. -// It is implemented by AppRun and DfltHarvestCfgr. -type HarvestConfigurer interface { - // ReportPeriods returns a map from the bitset of harvest types to the period that those types should be reported - ReportPeriods() map[HarvestTypes]time.Duration - // MaxSpanEvents returns the maximum number of Span Events that should be reported per period - MaxSpanEvents() int - // MaxCustomEvents returns the maximum number of Custom Events that should be reported per period - MaxCustomEvents() int - // MaxErrorEvents returns the maximum number of Error Events that should be reported per period - MaxErrorEvents() int - MaxTxnEventer -} - -// NewHarvest returns a new Harvest. -func NewHarvest(now time.Time, configurer HarvestConfigurer) *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()), - TxnEvents: newTxnEvents(configurer.MaxTxnEvents()), - ErrorEvents: newErrorEvents(configurer.MaxErrorEvents()), - } -} - -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) -} - -func createTrackUsageMetrics(metrics *metricTable) { - trackMutex.Lock() - defer trackMutex.Unlock() - - for _, m := range trackMetrics { - metrics.addSingleCount(m, forced) - } -} - -// CreateFinalMetrics creates extra metrics at harvest time. -func (h *Harvest) CreateFinalMetrics(reply *ConnectReply, hc HarvestConfigurer) { - 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) - - createTrackUsageMetrics(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 -} - -func supportMetric(metrics *metricTable, b bool, metricName string) { - if b { - metrics.addSingleCount(metricName, forced) - } -} - -// 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 { - caller = cat.Inbound.payloadCaller - } - 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 { - 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) - } - - supportMetric(metrics, args.AcceptPayloadSuccess, supportTracingAcceptSuccess) - supportMetric(metrics, args.AcceptPayloadException, supportTracingAcceptException) - supportMetric(metrics, args.AcceptPayloadParseException, supportTracingAcceptParseException) - supportMetric(metrics, args.AcceptPayloadCreateBeforeAccept, supportTracingCreateBeforeAccept) - supportMetric(metrics, args.AcceptPayloadIgnoredMultiple, supportTracingIgnoredMultiple) - supportMetric(metrics, args.AcceptPayloadIgnoredVersion, supportTracingIgnoredVersion) - supportMetric(metrics, args.AcceptPayloadUntrustedAccount, supportTracingAcceptUntrustedAccount) - supportMetric(metrics, args.AcceptPayloadNullPayload, supportTracingAcceptNull) - supportMetric(metrics, args.CreatePayloadSuccess, supportTracingCreatePayloadSuccess) - supportMetric(metrics, args.CreatePayloadException, supportTracingCreatePayloadException) - } - - // 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.HasErrors() { - metrics.addSingleCount(errorsRollupMetric.all, forced) - metrics.addSingleCount(errorsRollupMetric.webOrOther(args.IsWeb), forced) - metrics.addSingleCount(errorsPrefix+args.FinalName, forced) - } - - // Queueing Metrics - if args.Queuing > 0 { - metrics.addDuration(queueMetric, "", args.Queuing, args.Queuing, forced) - } -} - -// DfltHarvestCfgr implements HarvestConfigurer for internal test cases, and for situations where we don't -// have a ConnectReply, such as for serverless harvests -type DfltHarvestCfgr struct { - reportPeriods map[HarvestTypes]time.Duration - maxTxnEvents *uint - maxSpanEvents *uint - maxCustomEvents *uint - maxErrorEvents *uint -} - -// ReportPeriods returns a map from the bitset of harvest types to the period that those types should be reported -func (d *DfltHarvestCfgr) ReportPeriods() map[HarvestTypes]time.Duration { - if d.reportPeriods != nil { - return d.reportPeriods - } - return map[HarvestTypes]time.Duration{HarvestTypesAll: FixedHarvestPeriod} -} - -// MaxTxnEvents returns the maximum number of Transaction Events that should be reported per period -func (d *DfltHarvestCfgr) MaxTxnEvents() int { - if d.maxTxnEvents != nil { - return int(*d.maxTxnEvents) - } - return MaxTxnEvents -} - -// MaxSpanEvents returns the maximum number of Span Events that should be reported per period -func (d *DfltHarvestCfgr) MaxSpanEvents() int { - if d.maxSpanEvents != nil { - return int(*d.maxSpanEvents) - } - return MaxSpanEvents -} - -// MaxCustomEvents returns the maximum number of Custom Events that should be reported per period -func (d *DfltHarvestCfgr) MaxCustomEvents() int { - if d.maxCustomEvents != nil { - return int(*d.maxCustomEvents) - } - return MaxCustomEvents -} - -// MaxErrorEvents returns the maximum number of Error Events that should be reported per period -func (d *DfltHarvestCfgr) MaxErrorEvents() int { - if d.maxErrorEvents != nil { - return int(*d.maxErrorEvents) - } - return MaxErrorEvents -} diff --git a/internal/harvest_test.go b/internal/harvest_test.go deleted file mode 100644 index dd606b6b7..000000000 --- a/internal/harvest_test.go +++ /dev/null @@ -1,878 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "testing" - "time" -) - -func TestHarvestTimerAllFixed(t *testing.T) { - now := time.Now() - harvest := NewHarvest(now, &DfltHarvestCfgr{}) - 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) - } - } -} - -var one uint = 1 -var two uint = 2 -var three uint = 3 -var four uint = 4 - -func TestHarvestTimerAllConfigurable(t *testing.T) { - now := time.Now() - harvest := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - HarvestMetricsTraces: FixedHarvestPeriod, - HarvestTypesEvents: time.Second * 30, - }, - maxTxnEvents: &one, - maxCustomEvents: &two, - maxSpanEvents: &three, - maxErrorEvents: &four, - }) - 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 - nilHarvest.CreateFinalMetrics(nil, &DfltHarvestCfgr{}) - emptyHarvest := &Harvest{} - emptyHarvest.CreateFinalMetrics(nil, &DfltHarvestCfgr{}) - - 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 - } - } - }}`) - reply, err := ConstructConnectReply(replyJSON, PreconnectReply{}) - if err != nil { - t.Fatal(err) - } - var txnEvents uint = 22 - var customEvents uint = 33 - var errorEvents uint = 44 - var spanEvents uint = 55 - cfgr := &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - HarvestMetricsTraces: FixedHarvestPeriod, - HarvestTypesEvents: time.Second * 2, - }, - maxTxnEvents: &txnEvents, - maxCustomEvents: &customEvents, - maxErrorEvents: &errorEvents, - maxSpanEvents: &spanEvents, - } - h := NewHarvest(now, cfgr) - h.Metrics.addCount("rename_me", 1.0, unforced) - h.CreateFinalMetrics(reply, cfgr) - ExpectMetrics(t, h.Metrics, []WantMetric{ - {instanceReporting, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"been_renamed", "", false, []float64{1.0, 0, 0, 0, 0, 0}}, - {"Supportability/EventHarvest/ReportPeriod", "", true, []float64{1, 2, 2, 2, 2, 2 * 2}}, - {"Supportability/EventHarvest/AnalyticEventData/HarvestLimit", "", true, []float64{1, 22, 22, 22, 22, 22 * 22}}, - {"Supportability/EventHarvest/CustomEventData/HarvestLimit", "", true, []float64{1, 33, 33, 33, 33, 33 * 33}}, - {"Supportability/EventHarvest/ErrorEventData/HarvestLimit", "", true, []float64{1, 44, 44, 44, 44, 44 * 44}}, - {"Supportability/EventHarvest/SpanEventData/HarvestLimit", "", true, []float64{1, 55, 55, 55, 55, 55 * 55}}, - }) - - // Test again without any metric rules or event_harvest_config. - - replyJSON = []byte(`{"return_value":{ - }}`) - reply, err = ConstructConnectReply(replyJSON, PreconnectReply{}) - if err != nil { - t.Fatal(err) - } - h = NewHarvest(now, &DfltHarvestCfgr{}) - h.Metrics.addCount("rename_me", 1.0, unforced) - h.CreateFinalMetrics(reply, &DfltHarvestCfgr{}) - ExpectMetrics(t, h.Metrics, []WantMetric{ - {instanceReporting, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"rename_me", "", false, []float64{1.0, 0, 0, 0, 0, 0}}, - {"Supportability/EventHarvest/ReportPeriod", "", true, []float64{1, 60, 60, 60, 60, 60 * 60}}, - {"Supportability/EventHarvest/AnalyticEventData/HarvestLimit", "", true, []float64{1, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000 * 10 * 1000}}, - {"Supportability/EventHarvest/CustomEventData/HarvestLimit", "", true, []float64{1, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000, 10 * 1000 * 10 * 1000}}, - {"Supportability/EventHarvest/ErrorEventData/HarvestLimit", "", true, []float64{1, 100, 100, 100, 100, 100 * 100}}, - {"Supportability/EventHarvest/SpanEventData/HarvestLimit", "", true, []float64{1, 1000, 1000, 1000, 1000, 1000 * 1000}}, - }) -} - -func TestEmptyPayloads(t *testing.T) { - h := NewHarvest(time.Now(), &DfltHarvestCfgr{}) - payloads := h.Payloads(true) - if len(payloads) != 8 { - 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, &DfltHarvestCfgr{}) - 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, []WantMetric{}) -} - -func TestHarvestCustomEventsReady(t *testing.T) { - now := time.Now() - fixedHarvestTypes := HarvestMetricsTraces & HarvestTxnEvents & HarvestSpanEvents & HarvestErrorEvents - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - fixedHarvestTypes: FixedHarvestPeriod, - HarvestCustomEvents: time.Second * 5, - }, - maxCustomEvents: &three, - }) - 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, []WantEvent{{ - Intrinsics: map[string]interface{}{"type": "myEvent", "timestamp": MatchAnything}, - UserAttributes: params, - }}) - ExpectMetrics(t, h.Metrics, []WantMetric{ - {customEventsSeen, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {customEventsSent, "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) -} - -func TestHarvestTxnEventsReady(t *testing.T) { - now := time.Now() - fixedHarvestTypes := HarvestMetricsTraces & HarvestCustomEvents & HarvestSpanEvents & HarvestErrorEvents - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - fixedHarvestTypes: FixedHarvestPeriod, - HarvestTxnEvents: time.Second * 5, - }, - maxTxnEvents: &three, - }) - 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, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "finalName", - "totalTime": 2.0, - }, - }}) - ExpectMetrics(t, h.Metrics, []WantMetric{ - {txnEventsSeen, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {txnEventsSent, "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) -} - -func TestHarvestErrorEventsReady(t *testing.T) { - now := time.Now() - fixedHarvestTypes := HarvestMetricsTraces & HarvestCustomEvents & HarvestSpanEvents & HarvestTxnEvents - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - fixedHarvestTypes: FixedHarvestPeriod, - HarvestErrorEvents: time.Second * 5, - }, - maxErrorEvents: &three, - }) - h.ErrorEvents.Add(&ErrorEvent{ - ErrorData: ErrorData{Klass: "klass", Msg: "msg", When: time.Now()}, - TxnEvent: TxnEvent{FinalName: "finalName", Duration: 1 * 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 != "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, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": "klass", - "error.message": "msg", - "transactionName": "finalName", - }, - }}) - ExpectMetrics(t, h.Metrics, []WantMetric{ - {errorEventsSeen, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {errorEventsSent, "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) -} - -func TestHarvestSpanEventsReady(t *testing.T) { - now := time.Now() - fixedHarvestTypes := HarvestMetricsTraces & HarvestCustomEvents & HarvestTxnEvents & HarvestErrorEvents - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - fixedHarvestTypes: FixedHarvestPeriod, - HarvestSpanEvents: time.Second * 5, - }, - maxSpanEvents: &three, - }) - 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, []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, []WantMetric{ - {spanEventsSeen, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {spanEventsSent, "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) -} - -func TestHarvestMetricsTracesReady(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - HarvestMetricsTraces: FixedHarvestPeriod, - HarvestTypesEvents: time.Second * 65, - }, - maxTxnEvents: &one, - maxCustomEvents: &one, - maxErrorEvents: &one, - maxSpanEvents: &one, - }) - 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}) - - 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, []WantMetric{ - {"zip", "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) - ExpectMetrics(t, h.Metrics, []WantMetric{}) - - ExpectErrors(t, ready.ErrorTraces, []WantError{{ - TxnName: "finalName", - Msg: "msg", - Klass: "klass", - }}) - ExpectErrors(t, h.ErrorTraces, []WantError{}) - - ExpectSlowQueries(t, ready.SlowSQLs, []WantSlowQuery{{ - Count: 1, - MetricName: "Datastore/statement/MySQL/users/INSERT", - Query: "INSERT users", - TxnName: "finalName", - }}) - ExpectSlowQueries(t, h.SlowSQLs, []WantSlowQuery{}) - - ExpectTxnTraces(t, ready.TxnTraces, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - }}) - ExpectTxnTraces(t, h.TxnTraces, []WantTxnTrace{}) -} - -func TestMergeFailedHarvest(t *testing.T) { - start1 := time.Now() - start2 := start1.Add(1 * time.Minute) - - h := NewHarvest(start1, &DfltHarvestCfgr{}) - h.Metrics.addCount("zip", 1, forced) - h.TxnEvents.AddTxnEvent(&TxnEvent{ - FinalName: "finalName", - Start: time.Now(), - Duration: 1 * time.Second, - TotalTime: 2 * time.Second, - }, 0) - 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, - }) - 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.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, []WantMetric{ - {"zip", "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) - ExpectCustomEvents(t, h.CustomEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "type": "myEvent", - "timestamp": MatchAnything, - }, - UserAttributes: customEventParams, - }}) - ExpectErrorEvents(t, h.ErrorEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": "klass", - "error.message": "msg", - "transactionName": "finalName", - }, - }}) - ExpectTxnEvents(t, h.TxnEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "finalName", - "totalTime": 2.0, - }, - }}) - ExpectSpanEvents(t, h.SpanEvents, []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, []WantError{{ - TxnName: "finalName", - Msg: "msg", - Klass: "klass", - }}) - - nextHarvest := NewHarvest(start2, &DfltHarvestCfgr{}) - 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.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, []WantMetric{ - {"zip", "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) - ExpectCustomEvents(t, nextHarvest.CustomEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "type": "myEvent", - "timestamp": MatchAnything, - }, - UserAttributes: customEventParams, - }}) - ExpectErrorEvents(t, nextHarvest.ErrorEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": "klass", - "error.message": "msg", - "transactionName": "finalName", - }, - }}) - ExpectTxnEvents(t, nextHarvest.TxnEvents, []WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "finalName", - "totalTime": 2.0, - }, - }}) - ExpectSpanEvents(t, h.SpanEvents, []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, []WantError{}) -} - -func TestCreateTxnMetrics(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 - - 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, []WantMetric{ - {webName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {webRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {dispatcherMetric, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"WebTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"WebTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"Errors/all", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/allWeb", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/" + webName, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {apdexRollup, "", true, []float64{0, 1, 0, 2, 2, 0}}, - {"Apdex/zip/zap", "", false, []float64{0, 1, 0, 2, 2, 0}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 0, 0, 0, 0, 0}}, - {"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", "", false, []float64{1, 0, 0, 0, 0, 0}}, - }) - - args.FinalName = webName - args.IsWeb = true - args.Errors = nil - args.Zone = ApdexTolerating - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {webName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {webRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {dispatcherMetric, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"WebTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"WebTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {apdexRollup, "", true, []float64{0, 1, 0, 2, 2, 0}}, - {"Apdex/zip/zap", "", false, []float64{0, 1, 0, 2, 2, 0}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - }) - - args.FinalName = backgroundName - args.IsWeb = false - args.Errors = txnErrors - args.Zone = ApdexNone - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {backgroundName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {backgroundRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"OtherTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"OtherTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"Errors/all", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/allOther", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/" + backgroundName, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 0, 0, 0, 0, 0}}, - {"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", "", false, []float64{1, 0, 0, 0, 0, 0}}, - }) - - args.FinalName = backgroundName - args.IsWeb = false - args.Errors = nil - args.Zone = ApdexNone - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {backgroundName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {backgroundRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"OtherTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"OtherTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - {"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", "", false, []float64{1, 123, 123, 123, 123, 123 * 123}}, - }) - -} - -func TestHarvestSplitTxnEvents(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{}) - for i := 0; i < MaxTxnEvents; i++ { - h.TxnEvents.AddTxnEvent(&TxnEvent{}, Priority(float32(i))) - } - - payloadsWithSplit := h.Payloads(true) - payloadsWithoutSplit := h.Payloads(false) - - if len(payloadsWithSplit) != 9 { - t.Error(len(payloadsWithSplit)) - } - if len(payloadsWithoutSplit) != 8 { - 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.Zone = ApdexTolerating - metrics := newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {webName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {webRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {dispatcherMetric, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"WebTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"WebTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"Errors/all", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/allWeb", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/" + webName, "", true, []float64{1, 0, 0, 0, 0, 0}}, - {apdexRollup, "", true, []float64{0, 1, 0, 2, 2, 0}}, - {"Apdex/zip/zap", "", false, []float64{0, 1, 0, 2, 2, 0}}, - }) - - args.FinalName = webName - args.IsWeb = true - args.Errors = nil - args.Zone = ApdexTolerating - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {webName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {webRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {dispatcherMetric, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"WebTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"WebTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {apdexRollup, "", true, []float64{0, 1, 0, 2, 2, 0}}, - {"Apdex/zip/zap", "", false, []float64{0, 1, 0, 2, 2, 0}}, - }) - - args.FinalName = backgroundName - args.IsWeb = false - args.Errors = txnErrors - args.Zone = ApdexNone - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {backgroundName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {backgroundRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"OtherTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"OtherTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"Errors/all", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/allOther", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Errors/" + backgroundName, "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) - - args.FinalName = backgroundName - args.IsWeb = false - args.Errors = nil - args.Zone = ApdexNone - metrics = newMetricTable(100, time.Now()) - CreateTxnMetrics(args, metrics) - ExpectMetrics(t, metrics, []WantMetric{ - {backgroundName, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {backgroundRollup, "", true, []float64{1, 123, 0, 123, 123, 123 * 123}}, - {"OtherTransactionTotalTime", "", true, []float64{1, 150, 150, 150, 150, 150 * 150}}, - {"OtherTransactionTotalTime/zip/zap", "", false, []float64{1, 150, 150, 150, 150, 150 * 150}}, - }) -} - -func TestNewHarvestSetsDefaultValues(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{}) - - if cp := h.TxnEvents.capacity(); cp != MaxTxnEvents { - t.Error("wrong txn event capacity", cp) - } - if cp := h.CustomEvents.capacity(); cp != MaxCustomEvents { - t.Error("wrong custom event capacity", cp) - } - if cp := h.ErrorEvents.capacity(); cp != MaxErrorEvents { - t.Error("wrong error event capacity", cp) - } - if cp := h.SpanEvents.capacity(); cp != MaxSpanEvents { - t.Error("wrong span event capacity", cp) - } -} - -func TestNewHarvestUsesConnectReply(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - HarvestMetricsTraces: FixedHarvestPeriod, - HarvestTypesEvents: time.Second * 5, - }, - maxTxnEvents: &one, - maxCustomEvents: &two, - maxErrorEvents: &three, - maxSpanEvents: &four, - }) - - 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) - } -} - -func TestConfigurableHarvestZeroHarvestLimits(t *testing.T) { - now := time.Now() - - var zero uint - h := NewHarvest(now, &DfltHarvestCfgr{ - reportPeriods: map[HarvestTypes]time.Duration{ - HarvestMetricsTraces: FixedHarvestPeriod, - HarvestTypesEvents: time.Second * 5, - }, - maxTxnEvents: &zero, - maxCustomEvents: &zero, - maxErrorEvents: &zero, - maxSpanEvents: &zero, - }) - 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.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.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)) - } - } -} diff --git a/internal/integrationsupport/integrationsupport.go b/internal/integrationsupport/integrationsupport.go deleted file mode 100644 index f10bebd03..000000000 --- a/internal/integrationsupport/integrationsupport.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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 ( - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal" -) - -// AddAgentAttribute allows instrumentation packages to add agent attributes. -func AddAgentAttribute(txn newrelic.Transaction, id internal.AgentAttributeID, stringVal string, otherVal interface{}) { - if aa, ok := txn.(internal.AddAgentAttributer); ok { - aa.AddAgentAttribute(id, stringVal, otherVal) - } -} - -// AddAgentSpanAttribute allows instrumentation packages to add span attributes. -func AddAgentSpanAttribute(txn newrelic.Transaction, key internal.SpanAttribute, val string) { - internal.AddAgentSpanAttribute(txn, 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" -) - -// ExpectApp combines Application and Expect, for use in validating data in test apps -type ExpectApp interface { - internal.Expect - newrelic.Application -} - -// NewTestApp creates an ExpectApp with the given ConnectReply function and Config function -func NewTestApp(replyfn func(*internal.ConnectReply), cfgFn func(*newrelic.Config)) ExpectApp { - - cfg := newrelic.NewConfig(SampleAppName, testLicenseKey) - - if nil != cfgFn { - cfgFn(&cfg) - } - - // Prevent spawning app goroutines in tests. - if !cfg.ServerlessMode.Enabled { - cfg.Enabled = false - } - - app, err := newrelic.NewApplication(cfg) - if nil != err { - panic(err) - } - - internal.HarvestTesting(app, replyfn) - - return app.(ExpectApp) -} - -// 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 -} - -// SampleEverythingReplyFn is a reusable ConnectReply function that samples everything -var SampleEverythingReplyFn = func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} -} diff --git a/internal/intrinsics.go b/internal/intrinsics.go deleted file mode 100644 index c8a58b8a1..000000000 --- a/internal/intrinsics.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" -) - -func addOptionalStringField(w *jsonFieldsWriter, key, value string) { - if value != "" { - w.stringField(key, value) - } -} - -func intrinsicsJSON(e *TxnEvent, buf *bytes.Buffer) { - w := jsonFieldsWriter{buf: buf} - - buf.WriteByte('{') - - w.floatField("totalTime", e.TotalTime.Seconds()) - - if e.BetterCAT.Enabled { - w.stringField("guid", e.BetterCAT.ID) - w.stringField("traceId", e.BetterCAT.TraceID()) - w.writerField("priority", e.BetterCAT.Priority) - w.boolField("sampled", e.BetterCAT.Sampled) - } - - 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('}') -} diff --git a/internal/json_object_writer.go b/internal/json_object_writer.go deleted file mode 100644 index 2a5cc908d..000000000 --- a/internal/json_object_writer.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - - "github.com/newrelic/go-agent/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) 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) -} diff --git a/internal/jsonx/encode.go b/internal/jsonx/encode.go deleted file mode 100644 index 6495829f7..000000000 --- a/internal/jsonx/encode.go +++ /dev/null @@ -1,174 +0,0 @@ -// 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 -} - -// 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(']') -} diff --git a/internal/jsonx/encode_test.go b/internal/jsonx/encode_test.go deleted file mode 100644 index fed3ab7f7..000000000 --- a/internal/jsonx/encode_test.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package jsonx - -import ( - "bytes" - "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 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"`}, -} - -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) - } - } -} diff --git a/internal/labels.go b/internal/labels.go deleted file mode 100644 index 4c37f7963..000000000 --- a/internal/labels.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import "encoding/json" - -// Labels is used for connect JSON formatting. -type Labels map[string]string - -// MarshalJSON requires a comment for golint? -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) -} diff --git a/internal/limits.go b/internal/limits.go deleted file mode 100644 index 6ba0e192e..000000000 --- a/internal/limits.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 - // 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 - // 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 - // maxPayloadSizeInBytes specifies the maximum payload size in bytes that - // should be sent to any endpoint - maxPayloadSizeInBytes = 1000 * 1000 - - // 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 - // MaxCustomEvents is the maximum number of Transaction Events that can be captured - // per 60-second harvest cycle - MaxCustomEvents = 10 * 1000 - // MaxTxnEvents is the maximum number of Transaction Events that can be captured - // per 60-second harvest cycle - MaxTxnEvents = 10 * 1000 - maxRegularTraces = 1 - maxSyntheticsTraces = 20 - // MaxErrorEvents is the maximum number of Error Events that can be captured - // per 60-second harvest cycle - MaxErrorEvents = 100 - maxHarvestErrors = 20 - maxHarvestSlowSQLs = 10 - // MaxSpanEvents is the maximum number of Span Events that can be captured - // per 60-second harvest cycle - MaxSpanEvents = 1000 - - // attributes - attributeKeyLengthLimit = 255 - attributeValueLengthLimit = 255 - attributeUserLimit = 64 - // 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 - - txnNameCacheLimit = 40 -) diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index f5f13212d..000000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package logger - -import ( - "encoding/json" - "fmt" - "io" - "log" - "os" -) - -// 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 nil == err { - f.l.Print(string(js)) - } else { - f.l.Printf("unable to marshal log entry: %v", err) - } -} - -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 } diff --git a/internal/metric_names.go b/internal/metric_names.go deleted file mode 100644 index ce7ce8b2a..000000000 --- a/internal/metric_names.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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" - - webMetricPrefix = "WebTransaction/Go" - backgroundMetricPrefix = "OtherTransaction/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" - - // Distributed Tracing Supportability Metrics - supportTracingAcceptSuccess = "Supportability/DistributedTrace/AcceptPayload/Success" - supportTracingAcceptException = "Supportability/DistributedTrace/AcceptPayload/Exception" - supportTracingAcceptParseException = "Supportability/DistributedTrace/AcceptPayload/ParseException" - supportTracingCreateBeforeAccept = "Supportability/DistributedTrace/AcceptPayload/Ignored/CreateBeforeAccept" - supportTracingIgnoredMultiple = "Supportability/DistributedTrace/AcceptPayload/Ignored/Multiple" - supportTracingIgnoredVersion = "Supportability/DistributedTrace/AcceptPayload/Ignored/MajorVersion" - supportTracingAcceptUntrustedAccount = "Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount" - supportTracingAcceptNull = "Supportability/DistributedTrace/AcceptPayload/Ignored/Null" - supportTracingCreatePayloadSuccess = "Supportability/DistributedTrace/CreatePayload/Success" - supportTracingCreatePayloadException = "Supportability/DistributedTrace/CreatePayload/Exception" - - // 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" -) - -// DistributedTracingSupport is used to track distributed tracing activity for -// supportability. -type DistributedTracingSupport struct { - 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 -} - -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/") - - // 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 -} - -// customMetric 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 customMetric(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 -} - -// 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 -} - -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)) -} diff --git a/internal/metric_rules.go b/internal/metric_rules.go deleted file mode 100644 index 46ecc557a..000000000 --- a/internal/metric_rules.go +++ /dev/null @@ -1,167 +0,0 @@ -// 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 -} - -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}}" -) - -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 -} - -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 -} -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) - } -} - -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 -} diff --git a/internal/metric_rules_test.go b/internal/metric_rules_test.go deleted file mode 100644 index 841a836e1..000000000 --- a/internal/metric_rules_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// 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/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") - } -} diff --git a/internal/metrics.go b/internal/metrics.go deleted file mode 100644 index 24c10a700..000000000 --- a/internal/metrics.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "time" - - "github.com/newrelic/go-agent/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 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 -} diff --git a/internal/metrics_test.go b/internal/metrics_test.go deleted file mode 100644 index 47b356f1d..000000000 --- a/internal/metrics_test.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "fmt" - "testing" - "time" -) - -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, []WantMetric{ - {"apdex satisfied", "", false, []float64{2, 0, 0, 8, 9, 0}}, - {"apdex tolerated", "", false, []float64{0, 2, 0, 7, 8, 0}}, - {"one", "", false, []float64{2, 4, 2, 2, 2, 8}}, - {"apdex failed", "my_scope", false, []float64{0, 0, 1, 1, 1, 0}}, - {"one", "my_scope", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "my_scope", false, []float64{1, 4, 2, 4, 4, 16}}, - {"count 123", "", false, []float64{123, 0, 0, 0, 0, 0}}, - {"count 1", "", false, []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 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, []WantMetric{ - {"been_renamed", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"been_renamed", "scope1", false, []float64{1, 2, 1, 2, 2, 4}}, - {"been_renamed", "scope2", false, []float64{1, 2, 1, 2, 2, 4}}, - {"merge_me", "", false, []float64{2, 4, 2, 2, 2, 8}}, - }) -} - -func TestApplyEmptyRules(t *testing.T) { - js := `[]` - var rules 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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"one", "my_scope", false, []float64{1, 2, 1, 2, 2, 4}}, - }) -} - -func TestApplyNilRules(t *testing.T) { - var rules 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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"one", "my_scope", false, []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, []WantMetric{ - {"forced", "", true, []float64{1, 2, 2, 2, 2, 4}}, - {supportabilityDropped, "", true, []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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "", false, []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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "", false, []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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "", false, []float64{2, 4, 2, 2, 2, 8}}, - {"three", "", false, []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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "", false, []float64{2, 4, 2, 2, 2, 8}}, - {"three", "", false, []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, []WantMetric{ - {"one", "", false, []float64{1, 2, 1, 2, 2, 4}}, - {"two", "", false, []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, []WantMetric{ - {"zip", "", false, []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, []WantMetric{ - {"one", "my_scope", false, []float64{1, 0, 0, 0, 0, 0}}, - {"two", "my_scope", false, []float64{2, 4, 2, 2, 2, 8}}, - }) -} diff --git a/internal/obfuscate.go b/internal/obfuscate.go deleted file mode 100644 index 7ea3455c5..000000000 --- a/internal/obfuscate.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 -} diff --git a/internal/obfuscate_test.go b/internal/obfuscate_test.go deleted file mode 100644 index a7599bb60..000000000 --- a/internal/obfuscate_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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) - } - } -} diff --git a/internal/priority.go b/internal/priority.go deleted file mode 100644 index d785ff3bb..000000000 --- a/internal/priority.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -// 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" -) - -// NewPriority returns a new priority. -func NewPriority() Priority { - return Priority(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 -} diff --git a/internal/priority_test.go b/internal/priority_test.go deleted file mode 100644 index bb9309d94..000000000 --- a/internal/priority_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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) - } -} diff --git a/internal/queuing.go b/internal/queuing.go deleted file mode 100644 index 4a09e56d4..000000000 --- a/internal/queuing.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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{} -} - -// QueueDuration TODO -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) -} diff --git a/internal/queuing_test.go b/internal/queuing_test.go deleted file mode 100644 index a1d136b5b..000000000 --- a/internal/queuing_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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) - } -} diff --git a/internal/rand.go b/internal/rand.go deleted file mode 100644 index 94c0447d7..000000000 --- a/internal/rand.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 between 0.0 and 1.0. -func RandFloat32() float32 { - seededRand.Lock() - defer seededRand.Unlock() - - for { - if r := seededRand.Float32(); 0.0 != r { - return r - } - } -} - -// RandUint64N returns a random int64 that's -// between 0 and the passed in max, non-inclusive -func RandUint64N(max uint64) uint64 { - return RandUint64() % max -} diff --git a/internal/rules_cache.go b/internal/rules_cache.go deleted file mode 100644 index 31a5a0cf9..000000000 --- a/internal/rules_cache.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 -} diff --git a/internal/rules_cache_test.go b/internal/rules_cache_test.go deleted file mode 100644 index 992442578..000000000 --- a/internal/rules_cache_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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") -} diff --git a/internal/sampler.go b/internal/sampler.go deleted file mode 100644 index 8a4526f39..000000000 --- a/internal/sampler.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "runtime" - "time" - - "github.com/newrelic/go-agent/internal/logger" - "github.com/newrelic/go-agent/internal/sysinfo" -) - -// Sample is a system/runtime snapshot. -type Sample struct { - when time.Time - memStats runtime.MemStats - usage sysinfo.Usage - numGoroutine int - numCPU int -} - -func bytesToMebibytesFloat(bts uint64) float64 { - return float64(bts) / (1024 * 1024) -} - -// GetSample gathers a new Sample. -func GetSample(now time.Time, lg logger.Logger) *Sample { - s := Sample{ - 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) -} - -// Stats contains system information for a period of time. -type Stats struct { - numGoroutine int - allocBytes uint64 - heapObjects uint64 - user cpuStats - system cpuStats - gcPauseFraction float64 - deltaNumGC uint32 - deltaPauseTotal time.Duration - minPause time.Duration - maxPause time.Duration -} - -// Samples is used as the parameter to GetStats to avoid mixing up the previous -// and current sample. -type Samples struct { - Previous *Sample - Current *Sample -} - -// GetStats combines two Samples into a Stats. -func GetStats(ss Samples) Stats { - cur := ss.Current - prev := ss.Previous - elapsed := cur.when.Sub(prev.when) - - s := Stats{ - numGoroutine: cur.numGoroutine, - allocBytes: cur.memStats.Alloc, - heapObjects: cur.memStats.HeapObjects, - } - - // CPU Utilization - 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 Stats) 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) - } -} diff --git a/internal/sampler_test.go b/internal/sampler_test.go deleted file mode 100644 index 12f05830f..000000000 --- a/internal/sampler_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "testing" - "time" - - "github.com/newrelic/go-agent/internal/logger" -) - -func TestGetSample(t *testing.T) { - now := time.Now() - sample := GetSample(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 TestMetricsCreated(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{}) - - stats := Stats{ - 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, []WantMetric{ - {"Memory/Heap/AllocatedObjects", "", true, []float64{1, 5000, 5000, 5000, 5000, 25000000}}, - {"Memory/Physical", "", true, []float64{1, 37, 0, 37, 37, 1369}}, - {"CPU/User Time", "", true, []float64{1, 0.02, 0.02, 0.02, 0.02, 0.0004}}, - {"CPU/System Time", "", true, []float64{1, 0.04, 0.04, 0.04, 0.04, 0.0016}}, - {"CPU/User/Utilization", "", true, []float64{1, 0.01, 0, 0.01, 0.01, 0.0001}}, - {"CPU/System/Utilization", "", true, []float64{1, 0.02, 0, 0.02, 0.02, 0.0004}}, - {"Go/Runtime/Goroutines", "", true, []float64{1, 23, 23, 23, 23, 529}}, - {"GC/System/Pause Fraction", "", true, []float64{1, 3e-05, 0, 3e-05, 3e-05, 9e-10}}, - {"GC/System/Pauses", "", true, []float64{2, 0.0005, 0, 0.0001, 0.0004, 2.5e-7}}, - }) -} - -func TestMetricsCreatedEmpty(t *testing.T) { - now := time.Now() - h := NewHarvest(now, &DfltHarvestCfgr{}) - stats := Stats{} - - stats.MergeIntoHarvest(h) - - ExpectMetrics(t, h.Metrics, []WantMetric{ - {"Memory/Heap/AllocatedObjects", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Memory/Physical", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"CPU/User Time", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"CPU/System Time", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"CPU/User/Utilization", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"CPU/System/Utilization", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"Go/Runtime/Goroutines", "", true, []float64{1, 0, 0, 0, 0, 0}}, - {"GC/System/Pause Fraction", "", true, []float64{1, 0, 0, 0, 0, 0}}, - }) -} diff --git a/internal/security_policies.go b/internal/security_policies.go deleted file mode 100644 index aec1af208..000000000 --- a/internal/security_policies.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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) -} - -func isDisconnectSecurityPolicyError(e error) bool { - if _, ok := e.(errUnknownRequiredPolicy); ok { - return true - } - if _, ok := e.(errUnsetPolicy); ok { - return true - } - return false -} diff --git a/internal/security_policies_test.go b/internal/security_policies_test.go deleted file mode 100644 index 2ea2428c9..000000000 --- a/internal/security_policies_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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)) -} diff --git a/internal/segment_terms.go b/internal/segment_terms.go deleted file mode 100644 index 440908529..000000000 --- a/internal/segment_terms.go +++ /dev/null @@ -1,148 +0,0 @@ -// 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] -} diff --git a/internal/segment_terms_test.go b/internal/segment_terms_test.go deleted file mode 100644 index 4475bb8ba..000000000 --- a/internal/segment_terms_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// 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/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) - } - } -} diff --git a/internal/serverless.go b/internal/serverless.go deleted file mode 100644 index 9f339a588..000000000 --- a/internal/serverless.go +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "strings" - "sync" - "time" - - "github.com/newrelic/go-agent/internal/logger" -) - -const ( - lambdaMetadataVersion = 2 - - // AgentLanguage is used in the connect JSON and the Lambda JSON. - AgentLanguage = "go" -) - -// ServerlessHarvest is used to store and log data when the agent is running in -// serverless mode. -type ServerlessHarvest struct { - logger logger.Logger - version string - 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.Logger, version string, getEnv func(string) string) *ServerlessHarvest { - return &ServerlessHarvest{ - logger: logger, - version: version, - awsExecutionEnv: getEnv("AWS_EXECUTION_ENV"), - - // We can use a default HarvestConfigured parameter 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. - 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: sh.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 - } - - fmt.Fprintln(writer, string(js)) -} - -// 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 -} - -// 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) - } -} diff --git a/internal/serverless_test.go b/internal/serverless_test.go deleted file mode 100644 index 5c62d1f7f..000000000 --- a/internal/serverless_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "strings" - "testing" - "time" - - "github.com/newrelic/go-agent/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{}, "the-version", 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 != `"the-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{}, "the-version", 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{}, "the-version", 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()) - } - } -} diff --git a/internal/slow_queries.go b/internal/slow_queries.go deleted file mode 100644 index 527ed7fd2..000000000 --- a/internal/slow_queries.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "container/heap" - "hash/fnv" - "time" - - "github.com/newrelic/go-agent/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 -} diff --git a/internal/slow_queries_test.go b/internal/slow_queries_test.go deleted file mode 100644 index d471ed47b..000000000 --- a/internal/slow_queries_test.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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(sampleAttributeConfigInput, 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 := sampleAttributeConfigInput - 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(sampleAttributeConfigInput, 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: true, - ID: "txn-id", - Priority: 0.5, - }, - } - - txnEvent.BetterCAT.Inbound = &Payload{ - payloadCaller: payloadCaller{ - TransportType: "HTTP", - Type: "Browser", - App: "caller-app", - Account: "caller-account", - }, - ID: "caller-id", - TransactionID: "caller-parent-id", - TracedID: "trace-id", - TransportDuration: 2 * time.Second, - } - - 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.transportType": "HTTP", - "parent.transportDuration": 2, - "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) - } -} diff --git a/internal/span_events.go b/internal/span_events.go deleted file mode 100644 index ee0e4bafc..000000000 --- a/internal/span_events.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 = "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 - Category spanCategory - Component string - Kind string - IsEntrypoint bool - Attributes 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", e.Timestamp.UnixNano()/(1000*1000)) // in milliseconds - 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) - } - buf.WriteByte('}') - buf.WriteByte(',') - buf.WriteByte('{') - // user attributes section is unused - buf.WriteByte('}') - buf.WriteByte(',') - buf.WriteByte('{') - - w = jsonFieldsWriter{buf: buf} - for key, val := range e.Attributes { - w.writerField(key.String(), val) - } - - buf.WriteByte('}') - buf.WriteByte(']') -} - -// 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) addEvent(e *SpanEvent, cat *BetterCAT) { - e.TraceID = cat.TraceID() - e.TransactionID = cat.ID - e.Sampled = cat.Sampled - e.Priority = cat.Priority - events.addEventPopulated(e) -} - -func (events *spanEvents) addEventPopulated(e *SpanEvent) { - events.analyticsEvents.addEvent(analyticsEvent{priority: e.Priority, jsonWriter: e}) -} - -// MergeFromTransaction 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) MergeFromTransaction(txndata *TxnData) { - root := &SpanEvent{ - GUID: txndata.getRootSpanID(), - Timestamp: txndata.Start, - Duration: txndata.Duration, - Name: txndata.FinalName, - Category: spanCategoryGeneric, - IsEntrypoint: true, - } - if nil != txndata.BetterCAT.Inbound { - root.ParentID = txndata.BetterCAT.Inbound.ID - } - events.addEvent(root, &txndata.BetterCAT) - - for _, evt := range txndata.spanEvents { - events.addEvent(evt, &txndata.BetterCAT) - } -} - -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 -} diff --git a/internal/span_events_test.go b/internal/span_events_test.go deleted file mode 100644 index d108994b9..000000000 --- a/internal/span_events_test.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "testing" - "time" -) - -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.Attributes.addString(spanAttributeDBStatement, "SELECT * from foo") - e.Attributes.addString(spanAttributeDBInstance, "123") - e.Attributes.addString(spanAttributePeerAddress, "{host}:{portPathOrId}") - e.Attributes.addString(spanAttributePeerHostname, "host") - - expectEvent(t, &e, 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.Attributes.addString(spanAttributeDBStatement, "SELECT * from foo") - e.Attributes.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, 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.Attributes.addString(spanAttributeHTTPURL, "http://url.com") - e.Attributes.addString(spanAttributeHTTPMethod, "GET") - - expectEvent(t, &e, 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 TestSpanEventsMergeFromTransaction(t *testing.T) { - args := &TxnData{} - args.Start = time.Now() - args.Duration = 1 * time.Second - args.FinalName = "finalName" - args.BetterCAT.Sampled = true - args.BetterCAT.Priority = 0.7 - args.BetterCAT.Enabled = true - args.BetterCAT.ID = "txn-id" - args.BetterCAT.Inbound = &Payload{ - ID: "inbound-id", - TracedID: "inbound-trace-id", - } - args.rootSpanID = "root-span-id" - - args.spanEvents = []*SpanEvent{ - { - GUID: "span-1-id", - ParentID: "root-span-id", - Timestamp: time.Now(), - Duration: 3 * time.Millisecond, - Name: "span1", - Category: spanCategoryGeneric, - IsEntrypoint: false, - }, - { - GUID: "span-2-id", - ParentID: "span-1-id", - Timestamp: time.Now(), - Duration: 3 * time.Millisecond, - Name: "span2", - Category: spanCategoryGeneric, - IsEntrypoint: false, - }, - } - - spanEvents := newSpanEvents(10) - spanEvents.MergeFromTransaction(args) - - ExpectSpanEvents(t, spanEvents, []WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "finalName", - "sampled": true, - "priority": 0.7, - "category": spanCategoryGeneric, - "parentId": "inbound-id", - "nr.entryPoint": true, - "guid": "root-span-id", - "transactionId": "txn-id", - "traceId": "inbound-trace-id", - }, - }, - { - 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", - }, - }, - }) -} diff --git a/internal/sqlparse/sqlparse.go b/internal/sqlparse/sqlparse.go deleted file mode 100644 index 3e57aab91..000000000 --- a/internal/sqlparse/sqlparse.go +++ /dev/null @@ -1,63 +0,0 @@ -// 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" -) - -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. -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 - if nil != rg { - if m := rg.FindStringSubmatch(s); len(m) > 1 { - segment.Collection = extractTable(m[1]) - } - } - } -} diff --git a/internal/sqlparse/sqlparse_test.go b/internal/sqlparse/sqlparse_test.go deleted file mode 100644 index b7ed76a70..000000000 --- a/internal/sqlparse/sqlparse_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package sqlparse - -import ( - "testing" - - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/internal/crossagent" -) - -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"}, - } { - 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"}, - } { - 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) - } - } -} diff --git a/internal/stack_frame.go b/internal/stack_frame.go deleted file mode 100644 index 5087af003..000000000 --- a/internal/stack_frame.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package internal - -import "runtime" - -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 -} diff --git a/internal/stack_frame_pre_1_7.go b/internal/stack_frame_pre_1_7.go deleted file mode 100644 index ec13b255f..000000000 --- a/internal/stack_frame_pre_1_7.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build !go1.7 - -package internal - -import "runtime" - -func (st StackTrace) frames() []stacktraceFrame { - fs := make([]stacktraceFrame, len(st)) - for idx, pc := range st { - fs[idx] = lookupFrame(pc) - } - return fs -} - -func lookupFrame(pc uintptr) stacktraceFrame { - // The Golang runtime package documentation says "To look up the file - // and line number of the call itself, use pc[i]-1. As an exception to - // this rule, if pc[i-1] corresponds to the function runtime.sigpanic, - // then pc[i] is the program counter of a faulting instruction and - // should be used without any subtraction." - // - // TODO: Fully understand when this subtraction is necessary. - place := pc - 1 - f := runtime.FuncForPC(place) - if nil == f { - return stacktraceFrame{} - } - file, line := f.FileLine(place) - return stacktraceFrame{ - Name: f.Name(), - File: file, - Line: int64(line), - } -} diff --git a/internal/stacktrace.go b/internal/stacktrace.go deleted file mode 100644 index 67726d8f8..000000000 --- a/internal/stacktrace.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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/internal.") || - strings.Contains(f.Name, "github.com/newrelic/go-agent.") -} - -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(']') -} - -// 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 -} diff --git a/internal/stacktrace_test.go b/internal/stacktrace_test.go deleted file mode 100644 index 4802819da..000000000 --- a/internal/stacktrace_test.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/newrelic/go-agent/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.11 - inputFrames := []stacktraceFrame{ - { - File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/internal/stacktrace.go", - Name: "github.com/newrelic/go-agent/internal.GetStackTrace", - Line: 17, - }, - { - File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/internal_txn.go", - Name: "github.com/newrelic/go-agent.(*txn).NoticeError", - Line: 696, - }, - { - File: "\u003cautogenerated\u003e", - Name: "go.(*struct { github.com/newrelic/go-agent.threadWithExtras }).NoticeError", - Line: 1, - }, - { - File: "/Users/will/Desktop/gopath/src/github.com/newrelic/go-agent/internal_attributes_test.go", - Name: "github.com/newrelic/go-agent.TestAddAttributeSecurityPolicyDisablesInclude", - Line: 68, - }, - { - File: "/Users/will/.gvm/gos/go1.11/src/testing/testing.go", - Name: "testing.tRunner", - Line: 827, - }, - { - File: "/Users/will/.gvm/gos/go1.11/src/runtime/asm_amd64.s", - Name: "runtime.goexit", - Line: 1333, - }, - } - buf := &bytes.Buffer{} - writeFrames(buf, inputFrames) - expectedJSON := `[ - { - "name":"testing.tRunner", - "filepath":"/Users/will/.gvm/gos/go1.11/src/testing/testing.go", - "line":827 - }, - { - "name":"runtime.goexit", - "filepath":"/Users/will/.gvm/gos/go1.11/src/runtime/asm_amd64.s", - "line":1333 - } - ]` - 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/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)) - } -} diff --git a/internal/stacktracetest/stacktracetest.go b/internal/stacktracetest/stacktracetest.go deleted file mode 100644 index 57d2071f0..000000000 --- a/internal/stacktracetest/stacktracetest.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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() -} diff --git a/internal/synthetics_test.go b/internal/synthetics_test.go deleted file mode 100644 index 91d1d84b8..000000000 --- a/internal/synthetics_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/newrelic/go-agent/internal/cat" - "github.com/newrelic/go-agent/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, &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]) - } - } - } -} diff --git a/internal/sysinfo/bootid.go b/internal/sysinfo/bootid.go deleted file mode 100644 index bd90c8275..000000000 --- a/internal/sysinfo/bootid.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/docker.go b/internal/sysinfo/docker.go deleted file mode 100644 index 97deee896..000000000 --- a/internal/sysinfo/docker.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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" -) - -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() - - return parseDockerID(f) -} - -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 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 -} diff --git a/internal/sysinfo/docker_test.go b/internal/sysinfo/docker_test.go deleted file mode 100644 index 881aeea76..000000000 --- a/internal/sysinfo/docker_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// 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/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 { - t.Errorf("%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.") - } -} diff --git a/internal/sysinfo/errors.go b/internal/sysinfo/errors.go deleted file mode 100644 index 1aa784b47..000000000 --- a/internal/sysinfo/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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") -) diff --git a/internal/sysinfo/hostname_generic.go b/internal/sysinfo/hostname_generic.go deleted file mode 100644 index a33760554..000000000 --- a/internal/sysinfo/hostname_generic.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build !linux - -package sysinfo - -import "os" - -// Hostname returns the host name. -func Hostname() (string, error) { - return os.Hostname() -} diff --git a/internal/sysinfo/hostname_linux.go b/internal/sysinfo/hostname_linux.go deleted file mode 100644 index a826f200c..000000000 --- a/internal/sysinfo/hostname_linux.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package sysinfo - -import ( - "os" - "syscall" -) - -// Hostname returns the host name. -func Hostname() (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 -} diff --git a/internal/sysinfo/memtotal.go b/internal/sysinfo/memtotal.go deleted file mode 100644 index 31cc0919d..000000000 --- a/internal/sysinfo/memtotal.go +++ /dev/null @@ -1,43 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/memtotal_darwin.go b/internal/sysinfo/memtotal_darwin.go deleted file mode 100644 index 008befd7f..000000000 --- a/internal/sysinfo/memtotal_darwin.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/memtotal_darwin_test.go b/internal/sysinfo/memtotal_darwin_test.go deleted file mode 100644 index d90852f28..000000000 --- a/internal/sysinfo/memtotal_darwin_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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) - } -} diff --git a/internal/sysinfo/memtotal_freebsd.go b/internal/sysinfo/memtotal_freebsd.go deleted file mode 100644 index f97337099..000000000 --- a/internal/sysinfo/memtotal_freebsd.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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 - } -} diff --git a/internal/sysinfo/memtotal_freebsd_test.go b/internal/sysinfo/memtotal_freebsd_test.go deleted file mode 100644 index 82a3f1fda..000000000 --- a/internal/sysinfo/memtotal_freebsd_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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) - } -} diff --git a/internal/sysinfo/memtotal_linux.go b/internal/sysinfo/memtotal_linux.go deleted file mode 100644 index d5b0b8c72..000000000 --- a/internal/sysinfo/memtotal_linux.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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) -} diff --git a/internal/sysinfo/memtotal_openbsd_amd64.go b/internal/sysinfo/memtotal_openbsd_amd64.go deleted file mode 100644 index 2dc20960d..000000000 --- a/internal/sysinfo/memtotal_openbsd_amd64.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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 - } -} diff --git a/internal/sysinfo/memtotal_openbsd_test.go b/internal/sysinfo/memtotal_openbsd_test.go deleted file mode 100644 index e12140848..000000000 --- a/internal/sysinfo/memtotal_openbsd_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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) - } -} diff --git a/internal/sysinfo/memtotal_solaris.go b/internal/sysinfo/memtotal_solaris.go deleted file mode 100644 index fe2906903..000000000 --- a/internal/sysinfo/memtotal_solaris.go +++ /dev/null @@ -1,29 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/memtotal_solaris_test.go b/internal/sysinfo/memtotal_solaris_test.go deleted file mode 100644 index 739a9774f..000000000 --- a/internal/sysinfo/memtotal_solaris_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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") - } -} diff --git a/internal/sysinfo/memtotal_test.go b/internal/sysinfo/memtotal_test.go deleted file mode 100644 index 266d45b57..000000000 --- a/internal/sysinfo/memtotal_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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/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) - } - } -} diff --git a/internal/sysinfo/memtotal_windows.go b/internal/sysinfo/memtotal_windows.go deleted file mode 100644 index 984e033c5..000000000 --- a/internal/sysinfo/memtotal_windows.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/usage.go b/internal/sysinfo/usage.go deleted file mode 100644 index b744f65b5..000000000 --- a/internal/sysinfo/usage.go +++ /dev/null @@ -1,14 +0,0 @@ -// 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 -} diff --git a/internal/sysinfo/usage_posix.go b/internal/sysinfo/usage_posix.go deleted file mode 100644 index 4d758dea1..000000000 --- a/internal/sysinfo/usage_posix.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build !windows - -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 -} diff --git a/internal/sysinfo/usage_windows.go b/internal/sysinfo/usage_windows.go deleted file mode 100644 index 6012a0f80..000000000 --- a/internal/sysinfo/usage_windows.go +++ /dev/null @@ -1,37 +0,0 @@ -// 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 -} diff --git a/internal/tools/interface-wrapping/driver_conn.json b/internal/tools/interface-wrapping/driver_conn.json deleted file mode 100644 index f2076332e..000000000 --- a/internal/tools/interface-wrapping/driver_conn.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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" - ] -} diff --git a/internal/tools/interface-wrapping/driver_driver.json b/internal/tools/interface-wrapping/driver_driver.json deleted file mode 100644 index 81b57e283..000000000 --- a/internal/tools/interface-wrapping/driver_driver.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "comment": "used in wrapping driver.Driver", - "variable_name": "dv", - "test_variable_name": "dv.original", - "required_interfaces": [ - "driver.Driver" - ], - "optional_interfaces": [ - "driver.DriverContext" - ] -} diff --git a/internal/tools/interface-wrapping/driver_stmt.json b/internal/tools/interface-wrapping/driver_stmt.json deleted file mode 100644 index 2c569c238..000000000 --- a/internal/tools/interface-wrapping/driver_stmt.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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" - ] -} diff --git a/internal/tools/interface-wrapping/main.go b/internal/tools/interface-wrapping/main.go deleted file mode 100644 index 607f85ca3..000000000 --- a/internal/tools/interface-wrapping/main.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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("}") -} diff --git a/internal/tools/interface-wrapping/transaction_response_writer.json b/internal/tools/interface-wrapping/transaction_response_writer.json deleted file mode 100644 index a0d8f5be5..000000000 --- a/internal/tools/interface-wrapping/transaction_response_writer.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "comment": "used in internal_response_writer.go", - "variable_name": "thd", - "test_variable_name": "thd.txn.writer", - "required_interfaces": [ - "threadWithExtras" - ], - "optional_interfaces": [ - "http.CloseNotifier", - "http.Flusher", - "http.Hijacker", - "io.ReaderFrom" - ] -} diff --git a/internal/tools/rules/main.go b/internal/tools/rules/main.go deleted file mode 100644 index 0ee601258..000000000 --- a/internal/tools/rules/main.go +++ /dev/null @@ -1,50 +0,0 @@ -// 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/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) -} diff --git a/internal/tools/uncompress-serverless/main.go b/internal/tools/uncompress-serverless/main.go deleted file mode 100644 index a5d3f039a..000000000 --- a/internal/tools/uncompress-serverless/main.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "encoding/json" - "fmt" - "github.com/newrelic/go-agent/internal" - "os" -) - -// This tool will take an encoded, compressed serverless payload and print it out in a human readable format. -// To use it on a Mac with Bash, copy the payload to the clipboard (the whole thing - `[2,"NR_LAMBDA_MONITORING",{"metadata_version"...]` -// without backticks) and then run the app using pbpaste. Example from the root of the project directory: -// -// go run internal/tools/uncompress-serverless/main.go $(pbpaste) -func main() { - - compressed := []byte(os.Args[1]) - metadata, uncompressedData, e := internal.ParseServerlessPayload(compressed) - if nil != e { - panic(e) - } - js, _ := json.MarshalIndent(map[string]interface{}{"metadata": metadata, "data": uncompressedData}, "", " ") - fmt.Println(string(js)) - -} diff --git a/internal/trace_id_generator.go b/internal/trace_id_generator.go deleted file mode 100644 index ce5ca8028..000000000 --- a/internal/trace_id_generator.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - "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)), - } -} - -// GenerateTraceID creates a new trace identifier. -func (tg *TraceIDGenerator) GenerateTraceID() string { - tg.Lock() - defer tg.Unlock() - - u1 := tg.rnd.Uint32() - u2 := tg.rnd.Uint32() - bits := (uint64(u1) << 32) | uint64(u2) - return fmt.Sprintf("%016x", bits) -} diff --git a/internal/trace_id_generator_test.go b/internal/trace_id_generator_test.go deleted file mode 100644 index 98e018aed..000000000 --- a/internal/trace_id_generator_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// 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) - id := tg.GenerateTraceID() - if id != "d9466896a525ccbf" { - t.Error(id) - } -} diff --git a/internal/tracing.go b/internal/tracing.go deleted file mode 100644 index 8e4375c3d..000000000 --- a/internal/tracing.go +++ /dev/null @@ -1,771 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "errors" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/newrelic/go-agent/internal/cat" - "github.com/newrelic/go-agent/internal/jsonx" - "github.com/newrelic/go-agent/internal/logger" - "github.com/newrelic/go-agent/internal/sysinfo" -) - -// 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) -} - -// 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 { - FinalName string - Start time.Time - Duration time.Duration - TotalTime time.Duration - Queuing time.Duration - Zone ApdexZone - Attrs *Attributes - DatastoreExternalTotals - CrossProcess TxnCrossProcess - BetterCAT BetterCAT - HasError bool -} - -// BetterCAT stores the transaction's priority and all fields related -// to a DistributedTracer's Cross-Application Trace. -type BetterCAT struct { - Enabled bool - Priority Priority - Sampled bool - Inbound *Payload - ID string -} - -// TraceID returns the trace id. -func (e BetterCAT) TraceID() string { - if nil != e.Inbound { - return e.Inbound.TracedID - } - return e.ID -} - -// TxnData contains the recorded data of a transaction. -type TxnData struct { - TxnEvent - IsWeb bool - Name string // Work in progress name. - Errors TxnErrors // Lazily initialized. - Stop time.Time - ApdexThreshold time.Duration - - stamp segmentStamp - threadIDCounter uint64 - - TraceIDGenerator *TraceIDGenerator - LazilyCalculateSampled func() bool - SpanEventsEnabled bool - rootSpanID string - spanEvents []*SpanEvent - - customSegments map[string]*metricData - datastoreSegments map[DatastoreMetricKey]*metricData - externalSegments map[externalMetricKey]*metricData - messageSegments map[MessageMetricKey]*metricData - - TxnTrace - - SlowQueriesEnabled bool - 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 -} - -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) -} - -// Thread contains a segment stack that is used to track segment parenting time -// within a single goroutine. -type Thread struct { - threadID uint64 - stack []segmentFrame - // start and end are used to track the TotalTime this Thread 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 *Thread) 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 *Thread) TotalTime() time.Duration { - if thread.start.Before(thread.end) { - return thread.end.Sub(thread.start) - } - return 0 -} - -// NewThread returns a new Thread to track segments in a new goroutine. -func NewThread(txndata *TxnData) *Thread { - // Each thread needs a unique ID. - txndata.threadIDCounter++ - return &Thread{ - 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)) -} - -// spanAttributeMap is used for span attributes and segment attributes. The -// value is a jsonWriter to allow for segment query parameters. -type spanAttributeMap map[SpanAttribute]jsonWriter - -func (m *spanAttributeMap) addString(key SpanAttribute, val string) { - if "" != val { - m.add(key, stringJSONWriter(val)) - } -} - -func (m *spanAttributeMap) add(key SpanAttribute, 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 -} - -type segmentFrame struct { - segmentTime - children time.Duration - spanID string - attributes spanAttributeMap -} - -type segmentEnd struct { - start segmentTime - stop segmentTime - duration time.Duration - exclusive time.Duration - SpanID string - ParentID string - threadID uint64 - attributes 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, - Attributes: end.attributes, - IsEntrypoint: false, - } -} - -const ( - datastoreProductUnknown = "Unknown" - datastoreOperationUnknown = "other" -) - -// HasErrors indicates whether the transaction had errors. -func (t *TxnData) HasErrors() bool { - return len(t.Errors) > 0 -} - -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 *Thread) AddAgentSpanAttribute(key SpanAttribute, val string) { - if len(thread.stack) > 0 { - thread.stack[len(thread.stack)-1].attributes.addString(key, val) - } -} - -// StartSegment begins a segment. -func StartSegment(t *TxnData, thread *Thread, 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, - } -} - -func (t *TxnData) getRootSpanID() string { - if "" == t.rootSpanID { - t.rootSpanID = t.TraceIDGenerator.GenerateTraceID() - } - return t.rootSpanID -} - -// CurrentSpanIdentifier returns the identifier of the span at the top of the -// segment stack. -func (t *TxnData) CurrentSpanIdentifier(thread *Thread) string { - if 0 == len(thread.stack) { - return t.getRootSpanID() - } - if "" == thread.stack[len(thread.stack)-1].spanID { - thread.stack[len(thread.stack)-1].spanID = t.TraceIDGenerator.GenerateTraceID() - } - return thread.stack[len(thread.stack)-1].spanID -} - -func (t *TxnData) saveSpanEvent(e *SpanEvent) { - e.Attributes = t.Attrs.filterSpanAttributes(e.Attributes, destSpan) - if len(t.spanEvents) < MaxSpanEvents { - t.spanEvents = append(t.spanEvents, e) - } -} - -var ( - errMalformedSegment = errors.New("segment identifier malformed: perhaps unsafe code has modified it?") - errSegmentOrder = errors.New(`improper segment use: the Transaction must be used ` + - `in a single goroutine and segments must be ended in "last started first ended" order: ` + - `see https://github.com/newrelic/go-agent/blob/master/GUIDE.md#segments`) -) - -func endSegment(t *TxnData, thread *Thread, start SegmentStartTime, now time.Time) (segmentEnd, error) { - if 0 == start.Stamp { - 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, - attributes: frame.attributes, - } - 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 t.SpanEventsEnabled && t.LazilyCalculateSampled() { - s.SpanID = frame.spanID - if "" == s.SpanID { - s.SpanID = t.TraceIDGenerator.GenerateTraceID() - } - // 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 *Thread, start SegmentStartTime, now time.Time, name string) error { - end, err := endSegment(t, thread, start, now) - if nil != err { - 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.attributes.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 *Thread - Start SegmentStartTime - Now time.Time - Logger logger.Logger - Response *http.Response - URL *url.URL - Host string - Library string - Method string -} - -// EndExternalSegment ends an external segment. -func EndExternalSegment(p EndExternalParams) error { - t := p.TxnData - end, err := endSegment(t, p.Thread, p.Start, p.Now) - if nil != err { - 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]interface{}{ - "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 nil == t.externalSegments { - 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.attributes.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.Attributes.addString(spanAttributeHTTPURL, SafeURL(p.URL)) - evt.Attributes.addString(spanAttributeHTTPMethod, p.Method) - } - t.saveSpanEvent(evt) - } - - return nil -} - -// EndMessageParams contains the parameters for EndMessageSegment. -type EndMessageParams struct { - TxnData *TxnData - Thread *Thread - 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 nil != err { - return err - } - - key := MessageMetricKey{ - Library: p.Library, - DestinationType: p.DestinationType, - DestinationName: p.DestinationName, - DestinationTemp: p.DestinationTemp, - } - - if nil == t.messageSegments { - t.messageSegments = make(map[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.attributes.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 *Thread - Start SegmentStartTime - Now time.Time - Product string - Collection string - Operation string - ParameterizedQuery string - QueryParameters map[string]interface{} - Host string - PortPathOrID string - Database string -} - -const ( - unknownDatastoreHost = "unknown" - unknownDatastorePortPathOrID = "unknown" -) - -var ( - // ThisHost is the system hostname. - ThisHost = func() string { - if h, err := sysinfo.Hostname(); nil == err { - return h - } - return unknownDatastoreHost - }() - 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 nil != err { - 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 = 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 nil == p.TxnData.datastoreSegments { - 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.attributes.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.Attributes.addString(spanAttributeDBStatement, p.ParameterizedQuery) - evt.Attributes.addString(spanAttributeDBInstance, p.Database) - evt.Attributes.addString(spanAttributePeerAddress, datastoreSpanAddress(p.Host, p.PortPathOrID)) - evt.Attributes.addString(spanAttributePeerHostname, p.Host) - evt.Attributes.addString(spanAttributeDBCollection, p.Collection) - p.TxnData.saveSpanEvent(evt) - } - - return err -} - -// 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) - } -} diff --git a/internal/tracing_test.go b/internal/tracing_test.go deleted file mode 100644 index 7820a88bd..000000000 --- a/internal/tracing_test.go +++ /dev/null @@ -1,842 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/http" - "net/url" - "strconv" - "strings" - "testing" - "time" - - "github.com/newrelic/go-agent/internal/cat" - "github.com/newrelic/go-agent/internal/crossagent" - "github.com/newrelic/go-agent/internal/logger" -) - -func TestStartEndSegment(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - - txndata := &TxnData{} - thread := &Thread{} - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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 := &Thread{} - - 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, []WantMetric{ - {"Custom/alpha", "", false, []float64{1, 11, 7, 11, 11, 121}}, - {"Custom/t1", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t2", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t4", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/alpha", txndata.FinalName, false, []float64{1, 11, 7, 11, 11, 121}}, - {"Custom/t1", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t2", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t4", txndata.FinalName, false, []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 := &Thread{} - - 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, []WantMetric{ - {"Custom/t1", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t2", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t4", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t1", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t2", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t4", txndata.FinalName, false, []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 := &Thread{} - 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 := &Thread{} - 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: NewTraceIDGenerator(12345), - } - if id := txndata.getRootSpanID(); id != "d9466896a525ccbf" { - t.Error(id) - } - if id := txndata.getRootSpanID(); id != "d9466896a525ccbf" { - t.Error(id) - } -} - -func TestCurrentSpanIdentifier(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{ - TraceIDGenerator: NewTraceIDGenerator(12345), - } - thread := &Thread{} - id := txndata.CurrentSpanIdentifier(thread) - if id != "d9466896a525ccbf" { - 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 != "d9466896a525ccbf" { - 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 != "bcfb32e050b264b8" { - 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 := &Thread{} - - 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, []WantMetric{ - {"Custom/t1", "", false, []float64{2, 4, 3, 1, 3, 10}}, - {"Custom/t2", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", "", false, []float64{1, 2, 2, 2, 2, 4}}, - {"Custom/t1", txndata.FinalName, false, []float64{2, 4, 3, 1, 3, 10}}, - {"Custom/t2", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Custom/t3", txndata.FinalName, false, []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 := &Thread{} - - 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, []WantMetric{ - {"External/all", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"External/allWeb", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"External/f1/all", "", false, []float64{2, 4, 3, 1, 3, 10}}, - {"External/unknown/all", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"External/f1/http", txndata.FinalName, false, []float64{2, 4, 3, 1, 3, 10}}, - {"External/unknown/http", txndata.FinalName, false, []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, []WantMetric{ - {"External/all", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"External/allOther", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"External/f1/all", "", false, []float64{2, 4, 3, 1, 3, 10}}, - {"External/unknown/all", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"External/f1/http", txndata.FinalName, false, []float64{2, 4, 3, 1, 3, 10}}, - {"External/unknown/http", txndata.FinalName, false, []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 := &Thread{} - - 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, []WantMetric{ - {"Datastore/all", "", true, []float64{4, 6, 5, 1, 3, 12}}, - {"Datastore/allWeb", "", true, []float64{4, 6, 5, 1, 3, 12}}, - {"Datastore/MySQL/all", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/MySQL/allWeb", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/Unknown/all", "", true, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/Unknown/allWeb", "", true, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/operation/MySQL/SELECT", "", false, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/operation/MySQL/SELECT", txndata.FinalName, false, []float64{2, 4, 3, 1, 3, 10}}, - {"Datastore/operation/Unknown/other", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/operation/Unknown/other", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/statement/MySQL/my_table/SELECT", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/statement/MySQL/my_table/SELECT", txndata.FinalName, false, []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, []WantMetric{ - {"Datastore/all", "", true, []float64{4, 6, 5, 1, 3, 12}}, - {"Datastore/allOther", "", true, []float64{4, 6, 5, 1, 3, 12}}, - {"Datastore/MySQL/all", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/MySQL/allOther", "", true, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/Unknown/all", "", true, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/Unknown/allOther", "", true, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/operation/MySQL/SELECT", "", false, []float64{3, 5, 4, 1, 3, 11}}, - {"Datastore/operation/MySQL/SELECT", txndata.FinalName, false, []float64{2, 4, 3, 1, 3, 10}}, - {"Datastore/operation/Unknown/other", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/operation/Unknown/other", txndata.FinalName, false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/statement/MySQL/my_table/SELECT", "", false, []float64{1, 1, 1, 1, 1, 1}}, - {"Datastore/statement/MySQL/my_table/SELECT", txndata.FinalName, false, []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 := &Thread{} - - 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, - }) - - expect := strings.Replace(tc.ExpectedMetric, - tc.SystemHostname, ThisHost, -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, []WantMetric{ - {"Datastore/all", "", true, data}, - {"Datastore/allOther", "", true, data}, - {"Datastore/" + tc.Product + "/all", "", true, data}, - {"Datastore/" + tc.Product + "/allOther", "", true, data}, - {"Datastore/operation/" + tc.Product + "/SELECT", "", false, data}, - {"Datastore/statement/" + tc.Product + "/my_table/SELECT", "", false, data}, - {"Datastore/statement/" + tc.Product + "/my_table/SELECT", txndata.FinalName, false, data}, - {expect, "", false, data}, - }) - } -} - -func TestGenericSpanEventCreation(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{ - TraceIDGenerator: NewTraceIDGenerator(12345), - } - thread := &Thread{} - - // Enable that which is necessary to generate span events when segments are ended. - txndata.LazilyCalculateSampled = func() bool { return true } - txndata.SpanEventsEnabled = true - - 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 TestSpanEventNotSampled(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{ - TraceIDGenerator: NewTraceIDGenerator(12345), - } - thread := &Thread{} - - txndata.LazilyCalculateSampled = func() bool { return false } - txndata.SpanEventsEnabled = true - - 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 TestSpanEventNotEnabled(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{ - TraceIDGenerator: NewTraceIDGenerator(12345), - } - thread := &Thread{} - - txndata.LazilyCalculateSampled = func() bool { return true } - txndata.SpanEventsEnabled = false - - 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: NewTraceIDGenerator(12345), - } - thread := &Thread{} - - // Enable that which is necessary to generate span events when segments are ended. - txndata.LazilyCalculateSampled = func() bool { return true } - txndata.SpanEventsEnabled = true - - 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: NewTraceIDGenerator(12345), - } - thread := &Thread{} - - // Enable that which is necessary to generate span events when segments are ended. - txndata.LazilyCalculateSampled = func() bool { return true } - txndata.SpanEventsEnabled = true - - 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: NewTraceIDGenerator(12345), - } - txndata.CrossProcess.Enabled = true - thread := &Thread{} - - 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, []WantMetric{ - {"External/all", "", true, []float64{1, 3, 3, 3, 3, 9}}, - {"External/allOther", "", true, []float64{1, 3, 3, 3, 3, 9}}, - {"External/f1.com/all", "", false, []float64{1, 3, 3, 3, 3, 9}}, - {"External/f1.com/http", txndata.FinalName, false, []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: NewTraceIDGenerator(12345), - } - txndata.CrossProcess.Enabled = true - txndata.LazilyCalculateSampled = func() bool { return true } - txndata.SpanEventsEnabled = true - thread := &Thread{} - - 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, []WantMetric{ - {"MessageBroker/Kafka/Topic/Produce/Named/MyTopic", "WebTransaction/Go/zip", false, []float64{1, 2, 2, 2, 2, 4}}, - {"MessageBroker/Kafka/Topic/Produce/Named/MyTopic", "", false, []float64{1, 2, 2, 2, 2, 4}}, - }) -} diff --git a/internal/txn_cross_process.go b/internal/txn_cross_process.go deleted file mode 100644 index 3c101f720..000000000 --- a/internal/txn_cross_process.go +++ /dev/null @@ -1,420 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/newrelic/go-agent/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 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 - - // 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 -} - -// 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 -} - -// Init initialises a TxnCrossProcess based on the given application connect -// reply. -func (txp *TxnCrossProcess) Init(enabled bool, dt bool, reply *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 - } - - 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 0 != (txp.Type&txnCrossProcessSynthetics) && nil != txp.Synthetics -} - -// 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 - } - } - - 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) 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 -} diff --git a/internal/txn_cross_process_test.go b/internal/txn_cross_process_test.go deleted file mode 100644 index a100e5324..000000000 --- a/internal/txn_cross_process_test.go +++ /dev/null @@ -1,822 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - "net/http" - "reflect" - "testing" - "time" - - "github.com/newrelic/go-agent/internal/cat" -) - -var ( - replyAccountOne = &ConnectReply{ - CrossProcessID: "1#1", - EncodingKey: "foo", - TrustedAccounts: map[int]struct{}{1: {}}, - } - - replyAccountTwo = &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 *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 -} - -func TestTxnCrossProcessInit(t *testing.T) { - for _, tc := range []struct { - name string - enabled bool - reply *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 := "" - 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) - } - - actual.Init(tc.enabled, false, tc.reply) - err := actual.handleInboundRequestHeaders(CrossProcessMetadata{id, txnData, synthetics}) - - 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 *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"), - }, - }, - { - 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"), - }, - }, - { - 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"), - }, - }, - { - 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"), - }, - }, - } { - 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 *ConnectReply - metadata CrossProcessMetadata - expectedError bool - }{ - { - name: "disabled, invalid encoding key, invalid synthetics", - enabled: false, - reply: &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: &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) - } - } -} diff --git a/internal/txn_events.go b/internal/txn_events.go deleted file mode 100644 index cda3905b8..000000000 --- a/internal/txn_events.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "sort" - "strings" - "time" -) - -// DatastoreExternalTotals contains overview of external and datastore calls -// made during a transaction. -type DatastoreExternalTotals struct { - externalCallCount uint64 - externalDuration time.Duration - datastoreCallCount uint64 - datastoreDuration time.Duration -} - -// 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.floatField("timestamp", timeToFloatSeconds(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) - } -} - -// 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 { - w.stringField("parent.type", p.Type) - w.stringField("parent.app", p.App) - w.stringField("parent.account", p.Account) - w.stringField("parent.transportType", p.TransportType) - w.floatField("parent.transportDuration", p.TransportDuration.Seconds()) - } - - w.stringField("guid", e.BetterCAT.ID) - 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}, - } -} diff --git a/internal/txn_events_test.go b/internal/txn_events_test.go deleted file mode 100644 index 268937341..000000000 --- a/internal/txn_events_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "encoding/json" - "testing" - "time" - - "github.com/newrelic/go-agent/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, - ID: "txn-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, - ID: "txn-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":1.488393111e+09, - "error":false, - "duration":2, - "totalTime":3, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - }, - {}, - {}]`) -} - -func TestTxnEventMarshalWithApdex(t *testing.T) { - e := sampleTxnEvent - e.Zone = ApdexFailing - testTxnEventJSON(t, &e, `[ - { - "type":"Transaction", - "name":"myName", - "timestamp":1.488393111e+09, - "nr.apdexPerfZone":"F", - "error":false, - "duration":2, - "totalTime":3, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - }, - {}, - {}]`) -} - -func TestTxnEventMarshalWithDatastoreExternal(t *testing.T) { - e := sampleTxnEvent - e.DatastoreExternalTotals = DatastoreExternalTotals{ - externalCallCount: 22, - externalDuration: 1122334 * time.Millisecond, - datastoreCallCount: 33, - datastoreDuration: 5566778 * time.Millisecond, - } - testTxnEventJSON(t, &e, `[ - { - "type":"Transaction", - "name":"myName", - "timestamp":1.488393111e+09, - "error":false, - "duration":2, - "externalCallCount":22, - "externalDuration":1122.334, - "databaseCallCount":33, - "databaseDuration":5566.778, - "totalTime":3, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - }, - {}, - {}]`) -} - -func TestTxnEventMarshalWithInboundCaller(t *testing.T) { - e := sampleTxnEvent - e.BetterCAT.Inbound = &Payload{ - payloadCaller: payloadCaller{ - TransportType: "HTTP", - 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":"myName", - "timestamp":1.488393111e+09, - "error":false, - "duration":2, - "totalTime":3, - "parent.type": "Browser", - "parent.app": "caller-app", - "parent.account": "caller-account", - "parent.transportType": "HTTP", - "parent.transportDuration": 2, - "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{ - payloadCaller: payloadCaller{ - TransportType: "HTTP", - 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":1.488393111e+09, - "error":false, - "duration":2, - "totalTime":3 - }, - {}, - {}]`) -} - -func TestTxnEventMarshalWithAttributes(t *testing.T) { - aci := sampleAttributeConfigInput - aci.TransactionEvents.Exclude = append(aci.TransactionEvents.Exclude, "zap") - aci.TransactionEvents.Exclude = append(aci.TransactionEvents.Exclude, AttributeHostDisplayName.name()) - 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":1.488393111e+09, - "error":false, - "duration":2, - "totalTime":3, - "guid":"txn-id", - "traceId":"txn-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":1.488393111e+09, - "error":true, - "duration":2, - "totalTime":3, - "guid":"txn-id", - "traceId":"txn-id", - "priority":0.500000, - "sampled":false - }, - {}, - {}]`) -} diff --git a/internal/txn_trace.go b/internal/txn_trace.go deleted file mode 100644 index 7e155089b..000000000 --- a/internal/txn_trace.go +++ /dev/null @@ -1,453 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "bytes" - "container/heap" - "sort" - "time" - - "github.com/newrelic/go-agent/internal/jsonx" -) - -// See https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Trace-LEGACY.md - -type traceNodeHeap []traceNode - -type traceNodeParams struct { - attributes map[SpanAttribute]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.String(), 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) - 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 { - 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 -} diff --git a/internal/txn_trace_test.go b/internal/txn_trace_test.go deleted file mode 100644 index f8069e1d5..000000000 --- a/internal/txn_trace_test.go +++ /dev/null @@ -1,1286 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/http" - "strconv" - "testing" - "time" - - "github.com/newrelic/go-agent/internal/cat" - "github.com/newrelic/go-agent/internal/logger" -) - -func TestTxnTrace(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{} - thread := &Thread{} - 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(sampleAttributeConfigInput, 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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - 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": "txn-id", - "priority": 0.500000, - "sampled": false, - "totalTime": 30, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/t1", - RelativeStartMillis: 1000, - RelativeStopMillis: 6000, - Attributes: map[string]interface{}{}, - Children: []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: []WantTraceSegment{}, - }, - { - SegmentName: "External/example.com/http", - RelativeStartMillis: 4000, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{ - "http.url": "http://example.com/zip/zap", - }, - Children: []WantTraceSegment{}, - }, - }, - }, - { - SegmentName: "Custom/t4", - RelativeStartMillis: 7000, - RelativeStopMillis: 16000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/t5", - RelativeStartMillis: 8000, - RelativeStopMillis: 11000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/t6", - RelativeStartMillis: 9000, - RelativeStopMillis: 10000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - }, - }, - { - SegmentName: "Datastore/operation/MySQL/SELECT", - RelativeStartMillis: 12000, - RelativeStopMillis: 13000, - Attributes: map[string]interface{}{ - "db.statement": "'SELECT' on 'unknown' using 'MySQL'", - }, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "External/unknown/http", - RelativeStartMillis: 14000, - RelativeStopMillis: 15000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - }, - }, - { - SegmentName: "MessageBroker/Kafka/Topic/Produce/Named/MyTopic", - RelativeStartMillis: 17000, - RelativeStopMillis: 18000, - Attributes: map[string]interface{}{}, - Children: []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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - Intrinsics: map[string]interface{}{ - "guid": "txn-id", - "traceId": "txn-id", - "priority": 0.500000, - "sampled": false, - "totalTime": 30, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []WantTraceSegment{}, - }}, - }, - }}) -} - -func TestTxnTraceAsync(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{ - TraceIDGenerator: NewTraceIDGenerator(12345), - } - thread1 := &Thread{} - txndata.TxnTrace.Enabled = true - txndata.TxnTrace.StackTraceThreshold = 1 * time.Hour - txndata.TxnTrace.SegmentThreshold = 0 - txndata.BetterCAT.Sampled = true - txndata.SpanEventsEnabled = true - txndata.LazilyCalculateSampled = func() bool { return true } - - t1s1 := StartSegment(txndata, thread1, start.Add(1*time.Second)) - t1s2 := StartSegment(txndata, thread1, start.Add(2*time.Second)) - thread2 := NewThread(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 := NewThread(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(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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - Intrinsics: map[string]interface{}{ - "totalTime": 30, - "guid": "txn-id", - "traceId": "txn-id", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/thread1.segment1", - RelativeStartMillis: 1000, - RelativeStopMillis: 8000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/thread1.segment2", - RelativeStartMillis: 2000, - RelativeStopMillis: 4000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - }, - }, - { - SegmentName: "Custom/thread2.segment1", - RelativeStartMillis: 3000, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/thread3.segment1", - RelativeStartMillis: 6000, - RelativeStopMillis: 10000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/thread3.segment2", - RelativeStartMillis: 7000, - RelativeStopMillis: 9000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - }, - }, - }, - }}, - }, - }}) -} - -func TestTxnTraceOldCAT(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{} - thread := &Thread{} - 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(sampleAttributeConfigInput, 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, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - UserAttributes: map[string]interface{}{"zap": 123}, - AgentAttributes: map[string]interface{}{"request.uri": "/url"}, - Intrinsics: map[string]interface{}{"totalTime": 30}, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []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: []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 := sampleAttributeConfigInput - c.TransactionTracer.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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: tr.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - Intrinsics: map[string]interface{}{ - "totalTime": 0, - "guid": "txn-id", - "traceId": "txn-id", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []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(sampleAttributeConfigInput, 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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - MetricName: "WebTransaction/Go/hello", - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - Intrinsics: map[string]interface{}{ - "totalTime": 30, - "guid": "txn-id", - "traceId": "txn-id", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 20000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 20000}, - Children: []WantTraceSegment{}, - }}, - }, - }}) -} - -func TestTxnTraceSlowestNodesSaved(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{} - thread := &Thread{} - 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(sampleAttributeConfigInput, 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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - 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": "txn-id", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 123000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 123000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 123000}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/5", - RelativeStartMillis: 0, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/6", - RelativeStartMillis: 9000, - RelativeStopMillis: 15000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/7", - RelativeStartMillis: 18000, - RelativeStopMillis: 25000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/8", - RelativeStartMillis: 27000, - RelativeStopMillis: 35000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/9", - RelativeStartMillis: 36000, - RelativeStopMillis: 45000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - }, - }}, - }, - }}) -} - -func TestTxnTraceSegmentThreshold(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{} - thread := &Thread{} - 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(sampleAttributeConfigInput, 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, - ID: "txn-id", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - 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": "txn-id", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 123000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/hello", - RelativeStartMillis: 0, - RelativeStopMillis: 123000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 123000}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/7", - RelativeStartMillis: 18000, - RelativeStopMillis: 25000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/8", - RelativeStartMillis: 27000, - RelativeStopMillis: 35000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/9", - RelativeStartMillis: 36000, - RelativeStopMillis: 45000, - Attributes: map[string]interface{}{}, - Children: []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(sampleAttributeConfigInput, 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, - ID: "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, - ID: "txn-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, - ID: "txn-id-4", - Priority: 0.5, - }, - }, - Trace: txndata.TxnTrace, - }) - - ExpectTxnTraces(t, ht, []WantTxnTrace{{ - 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": "txn-id-5", - "priority": 0.500000, - "sampled": false, - }, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/5", - RelativeStartMillis: 0, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 5000}, - Children: []WantTraceSegment{}, - }}, - }, - }}) -} - -func TestTxnTraceStackTraceThreshold(t *testing.T) { - start := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) - txndata := &TxnData{} - thread := &Thread{} - 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, []WantTxnTrace{ - { - MetricName: "WebTransaction/Go/3", - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - Intrinsics: map[string]interface{}{"totalTime": 4}, - Root: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 3000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/3", - RelativeStartMillis: 0, - RelativeStopMillis: 3000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 3000}, - Children: []WantTraceSegment{ - { - SegmentName: "Custom/t1", - RelativeStartMillis: 1000, - RelativeStopMillis: 2000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "Custom/t2", - RelativeStartMillis: 2000, - RelativeStopMillis: 4000, - Attributes: map[string]interface{}{"backtrace": MatchAnything}, - Children: []WantTraceSegment{}, - }, - { - SegmentName: "External/example.com/http", - RelativeStartMillis: 4000, - RelativeStopMillis: 6000, - Attributes: map[string]interface{}{ - "backtrace": MatchAnything, - "http.url": "http://example.com/zip/zap", - }, - Children: []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(sampleAttributeConfigInput, 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, []WantTxnTrace{ - { - 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: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 3000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/3", - RelativeStartMillis: 0, - RelativeStopMillis: 3000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 3000}, - Children: []WantTraceSegment{}, - }}, - }, - }, - { - 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: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/5", - RelativeStartMillis: 0, - RelativeStopMillis: 5000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 5000}, - Children: []WantTraceSegment{}, - }}, - }, - }, - { - 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: WantTraceSegment{ - SegmentName: "ROOT", - RelativeStartMillis: 0, - RelativeStopMillis: 4000, - Attributes: map[string]interface{}{}, - Children: []WantTraceSegment{{ - SegmentName: "WebTransaction/Go/4", - RelativeStartMillis: 0, - RelativeStopMillis: 4000, - Attributes: map[string]interface{}{"exclusive_duration_millis": 4000}, - Children: []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, - ID: "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, - "guid":"this is guid", - "traceId":"this is guid", - "priority":0.000000, - "sampled":false - } - } - ],"this is guid",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, "") - } -} diff --git a/internal/url.go b/internal/url.go deleted file mode 100644 index dbc5ca76b..000000000 --- a/internal/url.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 -} diff --git a/internal/url_test.go b/internal/url_test.go deleted file mode 100644 index 9fafe8a65..000000000 --- a/internal/url_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "net/url" - "strings" - "testing" - - "github.com/newrelic/go-agent/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) - } -} diff --git a/internal/utilities.go b/internal/utilities.go deleted file mode 100644 index d993ab6c8..000000000 --- a/internal/utilities.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 timeToFloatSeconds(t time.Time) float64 { - return float64(t.UnixNano()) / float64(1000*1000*1000) -} - -func timeToFloatMilliseconds(t time.Time) float64 { - return float64(t.UnixNano()) / float64(1000*1000) -} - -// 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 -} - -func absTimeDiff(t1, t2 time.Time) time.Duration { - if t1.After(t2) { - return t1.Sub(t2) - } - return t2.Sub(t1) -} - -// 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] -} diff --git a/internal/utilities_test.go b/internal/utilities_test.go deleted file mode 100644 index 3c3cbded8..000000000 --- a/internal/utilities_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package internal - -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 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 TestAbsTimeDiff(t *testing.T) { - diff := 5 * time.Second - before := time.Now() - after := before.Add(5 * time.Second) - - if out := absTimeDiff(before, after); out != diff { - t.Error(out, diff) - } - if out := absTimeDiff(after, before); out != diff { - t.Error(out, diff) - } - if out := absTimeDiff(after, after); out != 0 { - t.Error(out) - } -} - -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) - } - } -} diff --git a/internal/utilization/addresses.go b/internal/utilization/addresses.go deleted file mode 100644 index 791b52726..000000000 --- a/internal/utilization/addresses.go +++ /dev/null @@ -1,76 +0,0 @@ -// 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://source.datanerd.us/java-agent/java_agent/blob/master/newrelic-agent/src/main/java/com/newrelic/agent/config/Hostname.java#L110 - conn, err := net.Dial("udp", "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 -} diff --git a/internal/utilization/aws.go b/internal/utilization/aws.go deleted file mode 100644 index 1cd8eeac5..000000000 --- a/internal/utilization/aws.go +++ /dev/null @@ -1,92 +0,0 @@ -// 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 ( - awsHostname = "169.254.169.254" - awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document" - awsEndpoint = "http://" + awsHostname + awsEndpointPath -) - -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) -} - -func getAWS(client *http.Client) (*aws, error) { - response, err := client.Get(awsEndpoint) - 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 -} diff --git a/internal/utilization/aws_test.go b/internal/utilization/aws_test.go deleted file mode 100644 index 33f5a3bc5..000000000 --- a/internal/utilization/aws_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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/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) - } - } - } -} diff --git a/internal/utilization/azure.go b/internal/utilization/azure.go deleted file mode 100644 index b2e1846be..000000000 --- a/internal/utilization/azure.go +++ /dev/null @@ -1,105 +0,0 @@ -// 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 _, ok := err.(unexpectedAzureErr); ok { - 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 -} diff --git a/internal/utilization/azure_test.go b/internal/utilization/azure_test.go deleted file mode 100644 index 9ca6b4ed8..000000000 --- a/internal/utilization/azure_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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/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) - } - } - } -} diff --git a/internal/utilization/fqdn.go b/internal/utilization/fqdn.go deleted file mode 100644 index 211312fa7..000000000 --- a/internal/utilization/fqdn.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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 "" -} diff --git a/internal/utilization/fqdn_pre18.go b/internal/utilization/fqdn_pre18.go deleted file mode 100644 index 44960fe00..000000000 --- a/internal/utilization/fqdn_pre18.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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 "" -} diff --git a/internal/utilization/gcp.go b/internal/utilization/gcp.go deleted file mode 100644 index b4fc4208e..000000000 --- a/internal/utilization/gcp.go +++ /dev/null @@ -1,155 +0,0 @@ -// 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] -} diff --git a/internal/utilization/gcp_test.go b/internal/utilization/gcp_test.go deleted file mode 100644 index 3e281d056..000000000 --- a/internal/utilization/gcp_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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/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) - } - } -} diff --git a/internal/utilization/pcf.go b/internal/utilization/pcf.go deleted file mode 100644 index f39c9646a..000000000 --- a/internal/utilization/pcf.go +++ /dev/null @@ -1,83 +0,0 @@ -// 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 -} diff --git a/internal/utilization/pcf_test.go b/internal/utilization/pcf_test.go deleted file mode 100644 index 172cdba0e..000000000 --- a/internal/utilization/pcf_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package utilization - -import ( - "testing" - - "github.com/newrelic/go-agent/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) - } - } - } -} diff --git a/internal/utilization/provider.go b/internal/utilization/provider.go deleted file mode 100644 index 03bcb64e0..000000000 --- a/internal/utilization/provider.go +++ /dev/null @@ -1,63 +0,0 @@ -// 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 = 1 * time.Second // 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') - } -} diff --git a/internal/utilization/provider_test.go b/internal/utilization/provider_test.go deleted file mode 100644 index 1b5c1fbcc..000000000 --- a/internal/utilization/provider_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// 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) { - for match, response := range m.responses { - if r.URL.String() == match { - 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) - } - } - } -} diff --git a/internal/utilization/utilization.go b/internal/utilization/utilization.go deleted file mode 100644 index a0fcbcb4a..000000000 --- a/internal/utilization/utilization.go +++ /dev/null @@ -1,242 +0,0 @@ -// 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/internal/logger" - "github.com/newrelic/go-agent/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 -} - -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) 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} - } - } - - if hostname, err := sysinfo.Hostname(); nil == err { - uDat.Hostname = hostname - } else { - warnGatherError("hostname", err) - } - - 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} - } -} diff --git a/internal/utilization/utilization_test.go b/internal/utilization/utilization_test.go deleted file mode 100644 index 098ecb62c..000000000 --- a/internal/utilization/utilization_test.go +++ /dev/null @@ -1,328 +0,0 @@ -// 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/internal/crossagent" - "github.com/newrelic/go-agent/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 || - data.Hostname == "" { - 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") - } -} diff --git a/internal_app.go b/internal_app.go deleted file mode 100644 index 1e9bcc2d5..000000000 --- a/internal_app.go +++ /dev/null @@ -1,574 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "errors" - "fmt" - "io" - "math" - "net/http" - "os" - "strings" - "sync" - "time" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/logger" -) - -type dataConsumer interface { - Consume(internal.AgentRunID, internal.Harvestable) -} - -type appData struct { - id internal.AgentRunID - data internal.Harvestable -} - -type app struct { - Logger - config Config - rpmControls internal.RpmControls - testHarvest *internal.Harvest - - // placeholderRun is used when the application is not connected. - placeholderRun *appRun - - // initiateShutdown is used to tell the processor to shutdown. - initiateShutdown chan struct{} - - // 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 internal.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 - - serverless *internal.ServerlessHarvest -} - -func (app *app) doHarvest(h *internal.Harvest, harvestStart time.Time, run *appRun) { - h.CreateFinalMetrics(run.Reply, run) - - payloads := h.Payloads(app.config.DistributedTracer.Enabled) - for _, p := range payloads { - cmd := p.EndpointMethod() - data, err := p.Data(run.Reply.RunID.String(), harvestStart) - - if nil != err { - app.Warn("unable to create harvest data", map[string]interface{}{ - "cmd": cmd, - "error": err.Error(), - }) - continue - } - if nil == data { - continue - } - - call := internal.RpmCmd{ - Collector: run.Reply.Collector, - RunID: run.Reply.RunID.String(), - Name: cmd, - Data: data, - RequestHeadersMap: run.Reply.RequestHeadersMap, - MaxPayloadSize: run.Reply.MaxPayloadSizeInBytes, - } - - resp := internal.CollectorRequest(call, app.rpmControls) - - if resp.IsDisconnect() || resp.IsRestartException() { - select { - case app.collectorErrorChan <- resp: - case <-app.shutdownStarted: - } - return - } - - if nil != resp.Err { - app.Warn("harvest failure", map[string]interface{}{ - "cmd": cmd, - "error": resp.Err.Error(), - "retain_data": resp.ShouldSaveHarvestData(), - }) - } - - if resp.ShouldSaveHarvestData() { - app.Consume(run.Reply.RunID, p) - } - } -} - -func (app *app) connectRoutine() { - connectAttempt := 0 - for { - reply, resp := internal.ConnectAttempt(config{app.config}, - app.config.SecurityPoliciesToken, app.config.HighSecurity, 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.Err { - app.Warn("application connect failure", map[string]interface{}{ - "error": resp.Err.Error(), - }) - } - - backoff := getConnectBackoffTime(connectAttempt) - time.Sleep(time.Duration(backoff) * time.Second) - connectAttempt++ - } -} - -// 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 *internal.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 <-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, errors.New("application shut down")) - - 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) - return - case resp := <-app.collectorErrorChan: - run = nil - h = nil - app.setState(nil, nil) - - if resp.IsDisconnect() { - app.setState(nil, resp.Err) - app.Error("application disconnected", map[string]interface{}{ - "app": app.config.AppName, - }) - } else if resp.IsRestartException() { - app.Info("application restarted", map[string]interface{}{ - "app": app.config.AppName, - }) - go app.connectRoutine() - } - case run = <-app.connectChan: - h = internal.NewHarvest(time.Now(), run) - app.setState(run, nil) - - app.Info("application connected", map[string]interface{}{ - "app": app.config.AppName, - "run": run.Reply.RunID.String(), - }) - processConnectMessages(run, app) - } - } -} - -func (app *app) Shutdown(timeout time.Duration) { - if !app.config.Enabled { - return - } - if app.config.ServerlessMode.Enabled { - return - } - - select { - case app.initiateShutdown <- struct{}{}: - 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 := internal.GetSample(time.Now(), app) - t := time.NewTicker(period) - for { - select { - case now := <-t.C: - current := internal.GetSample(now, app) - run, _ := app.getState() - app.Consume(run.Reply.RunID, internal.GetStats(internal.Samples{ - Previous: previous, - Current: current, - })) - previous = current - case <-app.shutdownStarted: - t.Stop() - return - } - } -} - -func (app *app) WaitForConnection(timeout time.Duration) error { - 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 != "" { - return nil - } - if time.Now().After(deadline) { - return fmt.Errorf("timeout out after %s", timeout.String()) - } - time.Sleep(pollPeriod) - } -} - -func newApp(c Config) (Application, error) { - c = copyConfigReferenceFields(c) - if err := c.Validate(); nil != err { - return nil, err - } - if nil == c.Logger { - c.Logger = logger.ShimLogger{} - } - app := &app{ - Logger: c.Logger, - config: c, - placeholderRun: newAppRun(c, internal.ConnectReplyDefaults()), - - // This channel must be buffered since Shutdown makes a - // non-blocking send attempt. - initiateShutdown: make(chan struct{}, 1), - - shutdownStarted: make(chan struct{}), - shutdownComplete: make(chan struct{}), - connectChan: make(chan *appRun, 1), - collectorErrorChan: make(chan internal.RPMResponse, 1), - dataChan: make(chan appData, internal.AppDataChanSize), - rpmControls: internal.RpmControls{ - License: c.License, - Client: &http.Client{ - Transport: c.Transport, - Timeout: internal.CollectorTimeout, - }, - Logger: c.Logger, - AgentVersion: Version, - }, - } - - app.Info("application created", map[string]interface{}{ - "app": app.config.AppName, - "version": Version, - "enabled": app.config.Enabled, - }) - - if app.config.Enabled { - if app.config.ServerlessMode.Enabled { - reply := newServerlessConnectReply(c) - app.run = newAppRun(c, reply) - app.serverless = internal.NewServerlessHarvest(c.Logger, Version, os.Getenv) - } else { - go app.process() - go app.connectRoutine() - if app.config.RuntimeSampler.Enabled { - go runSampler(app, internal.RuntimeSamplerPeriod) - } - } - } - - return app, nil -} - -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 = internal.NewHarvest(time.Now(), &internal.DfltHarvestCfgr{}) -} - -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 -} - -// StartTransaction implements newrelic.Application's StartTransaction. -func (app *app) StartTransaction(name string, w http.ResponseWriter, r *http.Request) Transaction { - run, _ := app.getState() - txn := upgradeTxn(newTxn(txnInput{ - app: app, - appRun: run, - writer: w, - Consumer: app, - }, name)) - - if nil != r { - txn.SetWebRequest(NewWebRequest(r)) - } - return txn -} - -var ( - errHighSecurityEnabled = errors.New("high security enabled") - errCustomEventsDisabled = errors.New("custom events disabled") - errCustomEventsRemoteDisabled = errors.New("custom events disabled by server") -) - -// RecordCustomEvent implements newrelic.Application's RecordCustomEvent. -func (app *app) RecordCustomEvent(eventType string, params map[string]interface{}) error { - if app.config.HighSecurity { - return errHighSecurityEnabled - } - - if !app.config.CustomInsightsEvents.Enabled { - return errCustomEventsDisabled - } - - event, e := internal.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 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, internal.CustomMetric{ - RawInputName: name, - Value: value, - }) - 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 internal.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) { - internal.ExpectCustomEvents(internal.ExtendValidator(t, "custom events"), app.testHarvest.CustomEvents, want) -} - -func (app *app) ExpectErrors(t internal.Validator, want []internal.WantError) { - t = internal.ExtendValidator(t, "traced errors") - internal.ExpectErrors(t, app.testHarvest.ErrorTraces, want) -} - -func (app *app) ExpectErrorEvents(t internal.Validator, want []internal.WantEvent) { - t = internal.ExtendValidator(t, "error events") - internal.ExpectErrorEvents(t, app.testHarvest.ErrorEvents, want) -} - -func (app *app) ExpectSpanEvents(t internal.Validator, want []internal.WantEvent) { - t = internal.ExtendValidator(t, "spans events") - internal.ExpectSpanEvents(t, app.testHarvest.SpanEvents, want) -} - -func (app *app) ExpectTxnEvents(t internal.Validator, want []internal.WantEvent) { - t = internal.ExtendValidator(t, "txn events") - internal.ExpectTxnEvents(t, app.testHarvest.TxnEvents, want) -} - -func (app *app) ExpectMetrics(t internal.Validator, want []internal.WantMetric) { - t = internal.ExtendValidator(t, "metrics") - internal.ExpectMetrics(t, app.testHarvest.Metrics, want) -} - -func (app *app) ExpectMetricsPresent(t internal.Validator, want []internal.WantMetric) { - t = internal.ExtendValidator(t, "metrics") - internal.ExpectMetricsPresent(t, app.testHarvest.Metrics, want) -} - -func (app *app) ExpectTxnMetrics(t internal.Validator, want internal.WantTxn) { - t = internal.ExtendValidator(t, "metrics") - internal.ExpectTxnMetrics(t, app.testHarvest.Metrics, want) -} - -func (app *app) ExpectTxnTraces(t internal.Validator, want []internal.WantTxnTrace) { - t = internal.ExtendValidator(t, "txn traces") - internal.ExpectTxnTraces(t, app.testHarvest.TxnTraces, want) -} - -func (app *app) ExpectSlowQueries(t internal.Validator, want []internal.WantSlowQuery) { - t = internal.ExtendValidator(t, "slow queries") - internal.ExpectSlowQueries(t, app.testHarvest.SlowSQLs, want) -} diff --git a/internal_app_test.go b/internal_app_test.go deleted file mode 100644 index 6f61c21c9..000000000 --- a/internal_app_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "fmt" - "testing" -) - -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) - } - } -} diff --git a/internal_attributes_test.go b/internal_attributes_test.go deleted file mode 100644 index d7e17a6fb..000000000 --- a/internal_attributes_test.go +++ /dev/null @@ -1,650 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "errors" - "net/http" - "net/url" - "testing" - - "github.com/newrelic/go-agent/internal" -) - -func TestAddAttributeHighSecurity(t *testing.T) { - cfgfn := func(cfg *Config) { - cfg.HighSecurity = true - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - - if err := txn.AddAttribute(`key`, 1); err != errHighSecurityEnabled { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - - if err := txn.AddAttribute(`key`, 1); err != errSecurityPolicy { - t.Error(err) - } - 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.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", nil, 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}, - UserAttributes: map[string]interface{}{}, - }}) -} - -func TestUserAttributeBasics(t *testing.T) { - cfgfn := func(cfg *Config) { - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - - txn.NoticeError(errors.New("zap")) - - if err := txn.AddAttribute(`int\key`, 1); nil != err { - t.Error(err) - } - if err := txn.AddAttribute(`str\key`, `zip\zap`); nil != err { - t.Error(err) - } - err := txn.AddAttribute("invalid_value", struct{}{}) - if _, ok := err.(internal.ErrInvalidAttributeType); !ok { - t.Error(err) - } - err = txn.AddAttribute("nil_value", nil) - if _, ok := err.(internal.ErrInvalidAttributeType); !ok { - t.Error(err) - } - txn.End() - if err := txn.AddAttribute("already_ended", "zap"); err != errAlreadyEnded { - t.Error(err) - } - - 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.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", nil, nil) - - txn.NoticeError(errors.New("zap")) - - if err := txn.AddAttribute("only_errors", 1); nil != err { - t.Error(err) - } - if err := txn.AddAttribute("only_txn_events", 2); nil != err { - t.Error(err) - } - if err := txn.AddAttribute("only_txn_traces", 3); nil != err { - t.Error(err) - } - if err := txn.AddAttribute("completed_excluded", 4); nil != err { - t.Error(err) - } - 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}, - }}) -} - -// 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 -} - -var ( - // Agent attributes expected in txn events from usualAttributeTestTransaction. - agent1 = map[string]interface{}{ - AttributeHostDisplayName: `my\host\display\name`, - AttributeResponseCode: `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", - 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 - if nil != cfgfn { - cfgfn(cfg) - } - }, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - txn.NoticeError(errors.New("zap")) - - hdr := txn.Header() - hdr.Set("Content-Type", `text/plain; charset=us-ascii`) - hdr.Set("Content-Length", `345`) - - txn.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 - }, 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, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, &http.Request{}) - txn.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}, - UserAttributes: map[string]interface{}{}, - }}) -} - -func TestNoResponseCode(t *testing.T) { - app := testApp(nil, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, &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 - }, 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 - }, 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 - }, 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, - AttributeRequestMethod, - AttributeRequestAccept, - AttributeRequestContentType, - AttributeRequestContentLength, - AttributeRequestHost, - AttributeRequestURI, - AttributeResponseContentType, - AttributeResponseContentLength, - AttributeHostDisplayName, - AttributeRequestUserAgent, - AttributeRequestReferer, - } -) - -func TestAgentAttributesExcluded(t *testing.T) { - agentAttributeTestcase(t, func(cfg *Config) { - cfg.Attributes.Exclude = allAgentAttributeNames - }, 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 - }, 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 - }, 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 - }, 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - u, err := url.Parse("/hello?remove=me") - if nil != err { - t.Error(err) - } - txn.SetWebRequest(customRequest{u: 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.Attributes.Exclude = append(cfg.Attributes.Exclude, AttributeRequestURI) - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - u, err := url.Parse("/hello?remove=me") - if nil != err { - t.Error(err) - } - txn.SetWebRequest(customRequest{u: 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, nil, t) - - txn := app.StartTransaction("hello1", nil, nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeMessageRoutingKey, "myRoutingKey", nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeMessageExchangeType, "myExchangeType", nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeMessageCorrelationID, "myCorrelationID", nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeMessageQueueName, "myQueueName", nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeMessageReplyTo, "myReplyTo", nil) - txn.End() - - txn = app.StartTransaction("hello2", nil, nil) - 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", - }, - }, - }) -} diff --git a/internal_benchmark_test.go b/internal_benchmark_test.go deleted file mode 100644 index 99de315b9..000000000 --- a/internal_benchmark_test.go +++ /dev/null @@ -1,225 +0,0 @@ -// 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, helloPath, handler)) - - w := newCompatibleResponseRecorder() - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - mux.ServeHTTP(w, helloRequest) - } -} - -// BenchmarkMuxWithNewRelic shows the overhead of instrumenting a request when -// the agent is disabled. -func BenchmarkMuxDisabledMode(b *testing.B) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - mux := http.NewServeMux() - mux.HandleFunc(WrapHandleFunc(app, 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) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - txn := app.StartTransaction("my txn", nil, nil) - fn := func() { - defer StartSegment(txn, "alpha").End() - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - fn() - } -} - -func BenchmarkTraceSegmentNoDefer(b *testing.B) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - txn := app.StartTransaction("my txn", nil, nil) - fn := func() { - s := StartSegment(txn, "alpha") - s.End() - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - fn() - } -} - -func BenchmarkTraceSegmentZeroSegmentThreshold(b *testing.B) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - cfg.TransactionTracer.SegmentThreshold = 0 - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - txn := app.StartTransaction("my txn", nil, nil) - fn := func() { - s := StartSegment(txn, "alpha") - s.End() - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - fn() - } -} - -func BenchmarkDatastoreSegment(b *testing.B) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - txn := app.StartTransaction("my txn", nil, nil) - 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) { - cfg := NewConfig("my app", sampleLicense) - cfg.Enabled = false - app, err := newApp(cfg) - if nil != err { - b.Fatal(err) - } - txn := app.StartTransaction("my txn", nil, nil) - 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", nil, nil) - StartSegment(txn, "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", nil, nil) - 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", nil, nil) - es := &ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: "http://example.com", - } - es.End() - txn.End() - } -} diff --git a/internal_browser_test.go b/internal_browser_test.go deleted file mode 100644 index dfb87d83d..000000000 --- a/internal_browser_test.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "reflect" - "testing" - - "github.com/newrelic/go-agent/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", nil, nil) - txn.WriteHeader(200) - txn.AddAttribute("zip", "zap") - hdr, err := txn.BrowserTimingHeader() - if nil != err { - t.Fatal(err) - } - - encodingKey := browserEncodingKey(testLicenseKey) - obfuscatedTxnName, _ := internal.Obfuscate([]byte("OtherTransaction/Go/hello"), encodingKey) - obfuscatedAttributes, _ := internal.Obfuscate([]byte(`{"u":{"zip":"zap"},"a":{"httpResponseCode":"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, _ := internal.Deobfuscate(hdr.info.TransactionName, encodingKey) - attr, _ := internal.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", nil, nil) - txn.WriteHeader(200) - txn.AddAttribute("zip", "zap") - hdr, err := txn.BrowserTimingHeader() - if nil != err { - t.Fatal(err) - } - - encodingKey := browserEncodingKey(testLicenseKey) - obfuscatedTxnName, _ := internal.Obfuscate([]byte("OtherTransaction/Go/hello"), encodingKey) - obfuscatedAttributes, _ := internal.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, _ := internal.Deobfuscate(hdr.info.TransactionName, encodingKey) - attr, _ := internal.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", nil, nil) - hdr, err := txn.BrowserTimingHeader() - if err != errBrowserDisabled { - t.Error(err) - } - if hdr.WithTags() != nil { - t.Error(hdr.WithTags()) - } -} - -func TestBrowserTimingHeaderNotConnected(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - hdr, err := txn.BrowserTimingHeader() - if err != nil { - // No error expected if the app is not yet connected. - t.Error(err) - } - if hdr.WithTags() != nil { - t.Error(hdr.WithTags()) - } -} - -func TestBrowserTimingHeaderAlreadyFinished(t *testing.T) { - app := testApp(browserReplyFields, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.End() - hdr, err := txn.BrowserTimingHeader() - if err != errAlreadyEnded { - t.Error(err) - } - if hdr.WithTags() != nil { - t.Error(hdr.WithTags()) - } -} - -func TestBrowserTimingHeaderTxnIgnored(t *testing.T) { - app := testApp(browserReplyFields, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.Ignore() - hdr, err := txn.BrowserTimingHeader() - if err != errTransactionIgnored { - t.Error(err) - } - if hdr.WithTags() != nil { - t.Error(hdr.WithTags()) - } -} - -func BenchmarkBrowserTimingHeaderSuccess(b *testing.B) { - app := testApp(browserReplyFields, nil, b) - txn := app.StartTransaction("my txn", nil, nil) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - hdr, err := txn.BrowserTimingHeader() - if nil == hdr || nil != err { - b.Fatal(hdr, err) - } - hdr.WithTags() - } -} diff --git a/internal_config.go b/internal_config.go deleted file mode 100644 index 6841605ce..000000000 --- a/internal_config.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/logger" - "github.com/newrelic/go-agent/internal/utilization" -) - -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 nil != cfg.ErrorCollector.IgnoreStatusCodes { - 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 nil != err { - return nil, err - } - fields := make(map[string]interface{}) - err = json.Unmarshal(js, &fields) - if nil != err { - 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" - } - - return json.Marshal(fields) -} - -func configConnectJSONInternal(c Config, pid int, util *utilization.Data, e internal.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 internal.Labels `json:"labels,omitempty"` - Environment internal.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: internal.AgentLanguage, - Version: version, - Host: internal.StringLengthByteLimit(util.Hostname, hostByteLimit), - HostDisplayName: internal.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.DefaultEventHarvestConfig(c), - }}) -} - -const ( - // https://source.datanerd.us/agents/agent-specs/blob/master/Connect-LEGACY.md#metadata-hash - metadataPrefix = "NEW_RELIC_METADATA_" -) - -func gatherMetadata(environ func() []string) map[string]string { - metadata := make(map[string]string) - env := environ() - 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 allows CreateConnectJSON to be a method on a non-public type. -type config struct{ Config } - -func (c config) CreateConnectJSON(securityPolicies *internal.SecurityPolicies) ([]byte, error) { - env := internal.NewEnvironment() - 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, - }, c.Logger) - return configConnectJSONInternal(c.Config, os.Getpid(), util, env, Version, securityPolicies, gatherMetadata(os.Environ)) -} diff --git a/internal_config_test.go b/internal_config_test.go deleted file mode 100644 index f9cb25771..000000000 --- a/internal_config_test.go +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "encoding/json" - "net/http" - "os" - "reflect" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/utilization" -) - -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 := NewConfig("my appname", "0123456789012345678901234567890123456789") - cfg.Labels["zip"] = "zap" - cfg.ErrorCollector.IgnoreStatusCodes = append(cfg.ErrorCollector.IgnoreStatusCodes, 405) - 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.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(`[ - { - "pid":123, - "language":"go", - "agent_version":"0.2.2", - "host":"my-hostname", - "settings":{ - "AppName":"my appname", - "Attributes":{"Enabled":true,"Exclude":["2"],"Include":["1"]}, - "BrowserMonitoring":{ - "Attributes":{"Enabled":false,"Exclude":["10"],"Include":["9"]}, - "Enabled":true - }, - "CrossApplicationTracer":{"Enabled":true}, - "CustomInsightsEvents":{"Enabled":true}, - "DatastoreTracer":{ - "DatabaseNameReporting":{"Enabled":true}, - "InstanceReporting":{"Enabled":true}, - "QueryParameters":{"Enabled":true}, - "SlowQuery":{ - "Enabled":true, - "Threshold":10000000 - } - }, - "DistributedTracer":{"Enabled":false}, - "Enabled":true, - "ErrorCollector":{ - "Attributes":{"Enabled":true,"Exclude":["6"],"Include":["5"]}, - "CaptureEvents":true, - "Enabled":true, - "IgnoreStatusCodes":[0,5,404,405] - }, - "HighSecurity":false, - "HostDisplayName":"", - "Labels":{"zip":"zap"}, - "Logger":"*logger.logFile", - "RuntimeSampler":{"Enabled":true}, - "SecurityPoliciesToken":"", - "ServerlessMode":{ - "AccountID":"", - "ApdexThreshold":500000000, - "Enabled":false, - "PrimaryAppID":"", - "TrustedAccountKey":"" - }, - "SpanEvents":{ - "Attributes":{ - "Enabled":true,"Exclude":["12"],"Include":["11"] - }, - "Enabled":true - }, - "TransactionEvents":{ - "Attributes":{"Enabled":true,"Exclude":["4"],"Include":["3"]}, - "Enabled":true, - "MaxSamplesStored": 10000 - }, - "TransactionTracer":{ - "Attributes":{"Enabled":true,"Exclude":["8"],"Include":["7"]}, - "Enabled":true, - "SegmentThreshold":2000000, - "Segments":{"Attributes":{"Enabled":true,"Exclude":["14"],"Include":["13"]}}, - "StackTraceThreshold":500000000, - "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.Compiler","comp"], - ["runtime.GOARCH","arch"], - ["runtime.GOOS","goos"], - ["runtime.Version","vers"], - ["runtime.NumCPU",8] - ], - "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": 10000, - "error_event_data": 100 - } - } - }]`) - - 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 nil != err { - t.Fatal(err) - } - - metadata := map[string]string{ - "NEW_RELIC_METADATA_ZAP": "zip", - } - js, err := configConnectJSONInternal(cp, 123, &utilization.SampleData, internal.SampleEnvironment, "0.2.2", sp.PointerIfPopulated(), metadata) - if nil != err { - t.Fatal(err) - } - out := standardizeNumbers(string(js)) - if out != expect { - t.Error(out) - } -} - -func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { - cfg := NewConfig("my appname", "0123456789012345678901234567890123456789") - cfg.Labels = nil - cfg.ErrorCollector.IgnoreStatusCodes = nil - - cp := copyConfigReferenceFields(cfg) - - expect := internal.CompactJSONString(`[ - { - "pid":123, - "language":"go", - "agent_version":"0.2.2", - "host":"my-hostname", - "settings":{ - "AppName":"my appname", - "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, - "BrowserMonitoring":{ - "Attributes":{ - "Enabled":false, - "Exclude":null, - "Include":null - }, - "Enabled":true - }, - "CrossApplicationTracer":{"Enabled":true}, - "CustomInsightsEvents":{"Enabled":true}, - "DatastoreTracer":{ - "DatabaseNameReporting":{"Enabled":true}, - "InstanceReporting":{"Enabled":true}, - "QueryParameters":{"Enabled":true}, - "SlowQuery":{ - "Enabled":true, - "Threshold":10000000 - } - }, - "DistributedTracer":{"Enabled":false}, - "Enabled":true, - "ErrorCollector":{ - "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, - "CaptureEvents":true, - "Enabled":true, - "IgnoreStatusCodes":null - }, - "HighSecurity":false, - "HostDisplayName":"", - "Labels":null, - "Logger":null, - "RuntimeSampler":{"Enabled":true}, - "SecurityPoliciesToken":"", - "ServerlessMode":{ - "AccountID":"", - "ApdexThreshold":500000000, - "Enabled":false, - "PrimaryAppID":"", - "TrustedAccountKey":"" - }, - "SpanEvents":{ - "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, - "Enabled":true - }, - "TransactionEvents":{ - "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, - "Enabled":true, - "MaxSamplesStored": 10000 - }, - "TransactionTracer":{ - "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, - "Enabled":true, - "SegmentThreshold":2000000, - "Segments":{"Attributes":{"Enabled":true,"Exclude":null,"Include":null}}, - "StackTraceThreshold":500000000, - "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.Compiler","comp"], - ["runtime.GOARCH","arch"], - ["runtime.GOOS","goos"], - ["runtime.Version","vers"], - ["runtime.NumCPU",8] - ], - "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": 10000, - "error_event_data": 100 - } - } - }]`) - - metadata := map[string]string{} - js, err := configConnectJSONInternal(cp, 123, &utilization.SampleData, internal.SampleEnvironment, "0.2.2", nil, metadata) - if nil != err { - t.Fatal(err) - } - out := standardizeNumbers(string(js)) - if out != expect { - t.Error(string(js)) - } -} - -func TestValidate(t *testing.T) { - c := Config{ - License: "0123456789012345678901234567890123456789", - AppName: "my app", - Enabled: true, - } - if err := c.Validate(); nil != err { - 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(); nil != err { - 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 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(func() []string { return nil }) - if !reflect.DeepEqual(metadata, map[string]string{}) { - t.Error(metadata) - } - metadata = gatherMetadata(func() []string { - return []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 := NewConfig("", "") - c.ServerlessMode.Enabled = true - if err := c.Validate(); nil != err { - t.Error(err) - } -} diff --git a/internal_context_test.go b/internal_context_test.go deleted file mode 100644 index 3502f172b..000000000 --- a/internal_context_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -package newrelic - -import ( - "net/http" - "testing" - - "github.com/newrelic/go-agent/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, nil, t) - _, h := WrapHandleFunc(app, "myTxn", func(rw http.ResponseWriter, r *http.Request) { - txn := FromContext(r.Context()) - segment := StartSegment(txn, "mySegment") - segment.End() - }) - req, _ := http.NewRequest("GET", "", nil) - h(nil, req) - - scope := "WebTransaction/Go/myTxn" - app.ExpectMetrics(t, []internal.WantMetric{ - {Name: "WebTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, - {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, - {Name: "WebTransactionTotalTime/Go/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/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, nil, t) - txn := app.StartTransaction("myTxn", nil, nil) - - 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(nil, nil, t) - txn := app.StartTransaction("myTxn", nil, nil) - - client := &http.Client{} - client.Transport = roundTripperFunc(func(*http.Request) (*http.Response, error) { - return &http.Response{}, nil - }) - client.Transport = NewRoundTripper(nil, 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: "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}, - }) -} diff --git a/internal_cross_process_test.go b/internal_cross_process_test.go deleted file mode 100644 index 254b99dcf..000000000 --- a/internal_cross_process_test.go +++ /dev/null @@ -1,244 +0,0 @@ -// 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" - - "errors" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/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 } - app := testApp(crossProcessReplyFn, cfgFn, nil) - clientTxn := app.StartTransaction("client", nil, nil) - 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 nil != err { - panic(err) - } - return req -} - -func outboundCrossProcessResponse() http.Header { - cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true } - app := testApp(crossProcessReplyFn, cfgFn, nil) - rw := httptest.NewRecorder() - txn := app.StartTransaction("txn", rw, inboundCrossProcessRequestFactory()) - txn.WriteHeader(200) - return rw.HeaderMap -} - -func TestCrossProcessWriteHeaderSuccess(t *testing.T) { - // Test that the CAT response header is present when the consumer uses - // txn.WriteHeader. - cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true } - app := testApp(crossProcessReplyFn, cfgFn, t) - w := httptest.NewRecorder() - txn := app.StartTransaction("hello", w, inboundCrossProcessRequestFactory()) - txn.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, - "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 } - app := testApp(crossProcessReplyFn, cfgFn, t) - w := httptest.NewRecorder() - txn := app.StartTransaction("hello", w, inboundCrossProcessRequestFactory()) - txn.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 TestCATRoundTripper(t *testing.T) { - cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = true } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - url := "http://example.com/" - 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") - }) - client.Transport = NewRoundTripper(txn, inner) - resp, err := client.Get(url) - 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.guid": internal.MatchAnything, - "nr.tripId": internal.MatchAnything, - "nr.pathHash": internal.MatchAnything, - }, - }}) -} - -func TestCrossProcessLocallyDisabled(t *testing.T) { - // Test that the CAT can be disabled by local configuration. - cfgFn := func(cfg *Config) { cfg.CrossApplicationTracer.Enabled = false } - app := testApp(crossProcessReplyFn, cfgFn, t) - w := httptest.NewRecorder() - txn := app.StartTransaction("hello", w, inboundCrossProcessRequestFactory()) - txn.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) {} - 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", w, inboundCrossProcessRequestFactory()) - txn.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 } - 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", w, inboundCrossProcessRequestFactory()) - txn.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{}{}, - }}) -} diff --git a/internal_distributed_trace_test.go b/internal_distributed_trace_test.go deleted file mode 100644 index 200cc0313..000000000 --- a/internal_distributed_trace_test.go +++ /dev/null @@ -1,1635 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "net/url" - "reflect" - "strings" - "testing" - "time" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/crossagent" -) - -type PayloadTest struct { - V *[2]int `json:"v,omitempty"` - D map[string]interface{} `json:"d,omitempty"` -} - -func distributedTracingReplyFields(reply *internal.ConnectReply) { - reply.AccountID = "123" - reply.AppID = "456" - reply.PrimaryAppID = "456" - reply.TrustedAccounts = map[int]struct{}{ - 123: {}, - } - reply.TrustedAccountKey = "123" - - reply.AdaptiveSampler = internal.SampleEverything{} -} - -func distributedTracingReplyFieldsNeedTrustKey(reply *internal.ConnectReply) { - reply.AccountID = "123" - reply.AppID = "456" - reply.PrimaryAppID = "456" - reply.TrustedAccounts = map[int]struct{}{ - 123: {}, - } - reply.TrustedAccountKey = "789" -} - -func makePayload(app Application, u *url.URL) DistributedTracePayload { - txn := app.StartTransaction("hello", nil, nil) - return txn.CreateDistributedTracePayload() -} - -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 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/DistributedTrace/AcceptPayload/Success", Scope: "", Forced: true, Data: singleCount}, - } -) - -func TestPayloadConnection(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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": ip.TransactionID, - "traceId": ip.TransactionID, - "parentSpanId": ip.ID, - "guid": internal.MatchAnything, - "sampled": internal.MatchAnything, - "priority": internal.MatchAnything, - }, - }}) -} - -func TestAcceptMultiple(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if nil != err { - t.Error(err) - } - err = txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if err != errAlreadyAccepted { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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": ip.TransactionID, - "traceId": ip.TransactionID, - "parentSpanId": ip.ID, - "guid": internal.MatchAnything, - "sampled": internal.MatchAnything, - "priority": internal.MatchAnything, - }, - }}) -} - -func TestPayloadConnectionText(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload.Text()) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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": ip.TransactionID, - "traceId": ip.TransactionID, - "parentSpanId": ip.ID, - "guid": internal.MatchAnything, - "sampled": internal.MatchAnything, - "priority": internal.MatchAnything, - }, - }}) -} - -func validBase64(s string) bool { - _, err := base64.StdEncoding.DecodeString(s) - return err == nil -} - -func TestPayloadConnectionHTTPSafe(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - p := payload.HTTPSafe() - if !validBase64(p) { - t.Error(p) - } - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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": ip.TransactionID, - "traceId": ip.TransactionID, - "parentSpanId": ip.ID, - "guid": internal.MatchAnything, - "sampled": internal.MatchAnything, - "priority": internal.MatchAnything, - }, - }}) -} - -func TestPayloadConnectionNotConnected(t *testing.T) { - app := testApp(nil, enableBetterCAT, t) - payload := makePayload(app, nil) - txn := app.StartTransaction("hello", nil, nil) - if nil == payload { - t.Fatal(payload) - } - if "" != payload.Text() { - t.Error(payload.Text()) - } - if "" != payload.HTTPSafe() { - t.Error(payload.HTTPSafe()) - } - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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 TestPayloadConnectionBetterCatDisabled(t *testing.T) { - app := testApp(nil, disableCAT, t) - payload := makePayload(app, nil) - txn := app.StartTransaction("hello", nil, nil) - if nil == payload { - t.Fatal(payload) - } - if "" != payload.Text() { - t.Error(payload.Text()) - } - if "" != payload.HTTPSafe() { - t.Error(payload.HTTPSafe()) - } - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if err == nil { - t.Error("missing expected error") - } - if errInboundPayloadDTDisabled != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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", nil, nil) - - payload := txn.CreateDistributedTracePayload() - if nil == payload { - t.Fatal(payload) - } - if "" != payload.Text() { - t.Error(payload.Text()) - } - if "" != payload.HTTPSafe() { - t.Error(payload.HTTPSafe()) - } - err := txn.End() - if nil != err { - t.Error(err) - } -} - -func TestPayloadConnectionEmptyString(t *testing.T) { - app := testApp(nil, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, "") - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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", nil, nil) - txn.End() - payload := txn.CreateDistributedTracePayload() - if nil == payload { - t.Fatal(payload) - } - if "" != payload.Text() { - t.Error(payload.Text()) - } - if "" != payload.HTTPSafe() { - t.Error(payload.HTTPSafe()) - } -} - -func TestAcceptPayloadFinished(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - txn := app.StartTransaction("hello", nil, nil) - err := txn.End() - if nil != err { - t.Error(err) - } - err = txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if err != errAlreadyEnded { - t.Fatal(err) - } - 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 TestPayloadTypeUnknown(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - invalidPayload := 22 - err := txn.AcceptDistributedTracePayload(TransportHTTP, invalidPayload) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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) - payload := makePayload(app, nil) - txn := app.StartTransaction("hello", nil, nil) - txn.CreateDistributedTracePayload() - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if errOutboundPayloadCreated != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/CreatePayload/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", nil, nil) - err := txn.AcceptDistributedTracePayload(emptyTransport, - `{ - "v":[0,1], - "d":{ - "ty":"App", - "ap":"456", - "ac":"123", - "id":"id", - "tr":"traceID", - "ti":1488325987402 - } - }`) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, - `{ - "v":[100,0], - "d":{ - "ty":"App", - "ap":"456", - "ac":"123", - "ti":1488325987402 - } - }`) - if nil == err { - t.Error("missing expected error here") - } - err = txn.End() - if nil != err { - t.Error(err) - } - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/MajorVersion", Scope: "", Forced: true, Data: singleCount}, - }, backgroundMetricsUnknownCaller...)) - 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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, - `{ - "v":[0,1], - "d":[] - }`) - if nil == err { - t.Error("missing expected parsing error") - } - err = txn.End() - if nil != err { - t.Error(err) - } - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: singleCount}, - }, backgroundMetricsUnknownCaller...)) - 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) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - ip.Timestamp.Set(time.Now().Add(1 * time.Hour)) - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, ip) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - 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": ip.TransactionID, - "traceId": ip.TransactionID, - "parentSpanId": ip.ID, - "guid": internal.MatchAnything, - "sampled": internal.MatchAnything, - "priority": internal.MatchAnything, - }, - }}) -} - -func TestPayloadUntrustedAccount(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - ip, ok := payload.(internal.Payload) - if !ok { - t.Fatal(payload) - } - ip.Account = "12345" - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, ip) - - if err != errTrustedAccountKey { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/Ignored/UntrustedAccount", 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 TestPayloadMissingVersion(t *testing.T) { - // ensures that a complete distributed trace payload without a version fails - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, - `{ - "d":{ - "ty":"App", - "ap":"456", - "ac":"123", - "id":"id", - "tr":"traceID", - "ti":1488325987402 - } - }`) - if nil == err { - t.Log("Expected error from missing Version (v)") - t.Fail() - } - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if err != errTrustedAccountKey { - t.Error("Expected ErrTrustedAccountKey from mismatched trustkeys", err) - } - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if err != errTrustedAccountKey { - t.Error("Expected ErrTrustedAccountKey from mismatched trustkeys", err) - } - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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}, - } -) - -func TestNilPayload(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, nil) - - if nil != err { - t.Error(err) - } - - err = txn.End() - if nil != err { - t.Error(err) - } - - 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", nil, nil) - txn.NoticeError(errors.New("oh no")) - - err := txn.End() - if nil != err { - t.Error(err) - } - - 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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - - if nil == err { - t.Log("Expected error from missing guid and transactionId") - t.Fail() - } - - err = txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, - }, backgroundUnknownCaller...)) -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - - if nil == err { - t.Log("Expected error from missing version") - t.Fail() - } - - err = txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, - }, backgroundUnknownCaller...)) -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - - if nil == err { - t.Log("Expected error from missing ac field") - t.Fail() - } - - err = txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, - }, backgroundUnknownCaller...)) -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - - if nil == err { - t.Log("Expected error from invalid json") - t.Fail() - } - - err = txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/AcceptPayload/ParseException", Scope: "", Forced: true, Data: nil}, - }, backgroundUnknownCaller...)) -} - -func TestErrorsByCaller(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - - txn := app.StartTransaction("hello", nil, nil) - payload := makePayload(app, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - - if nil != err { - t.Error(err) - } - - txn.NoticeError(errors.New("oh no")) - - err = txn.End() - if nil != err { - t.Error(err) - } - - 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/DistributedTrace/AcceptPayload/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", nil, nil) - - p := txn.CreateDistributedTracePayload() - - // empty/shim payload objects return empty strings - if "" != p.Text() { - t.Log("Non empty string response for .Text() method") - t.Fail() - } - - if "" != p.HTTPSafe() { - t.Log("Non empty string response for .HTTPSafe() method") - t.Fail() - } - - err := txn.End() - if nil != err { - t.Error(err) - } - - 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", nil, nil) - - p := txn.CreateDistributedTracePayload() - - // empty/shim payload objects return empty strings - if "" != p.Text() { - t.Log("Non empty string response for .Text() method") - t.Fail() - } - - if "" != p.HTTPSafe() { - t.Log("Non empty string response for .HTTPSafe() method") - t.Fail() - } - - err := txn.End() - if nil != err { - t.Error(err) - } - - 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", nil, nil) - - p := txn.CreateDistributedTracePayload() - - // empty/shim payload objects return empty strings - if "" == p.Text() { - t.Log("Empty string response for .Text() method") - t.Fail() - } - - if "" == p.HTTPSafe() { - t.Log("Empty string response for .HTTPSafe() method") - t.Fail() - } - - err := txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/CreatePayload/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 testPayloadFieldsPresent(t *testing.T, p DistributedTracePayload, keys ...string) { - out := struct { - Version []int `json:"v"` - Data map[string]interface{} `json:"d"` - }{} - if err := json.Unmarshal([]byte(p.Text()), &out); nil != err { - t.Fatal("unable to unmarshal payload Text", err) - } - 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", nil, nil) - - p := txn.CreateDistributedTracePayload() - - testPayloadFieldsPresent(t, p, "ty", "ac", "ap", "tr", "ti") - - err := txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/CreatePayload/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 - var payloadData PayloadTest - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - - p := txn.CreateDistributedTracePayload() - - if err := json.Unmarshal([]byte(p.Text()), &payloadData); nil != err { - t.Log("Could not marshall payload into test struct") - t.Error(err) - } - - if nil != payloadData.D["tk"] { - t.Log("Did not expect trust key (tk) to be there") - t.Log(p.Text()) - t.Fail() - } - - err := txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/CreatePayload/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 - var payloadData PayloadTest - app := testApp(distributedTracingReplyFieldsNeedTrustKey, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - - p := txn.CreateDistributedTracePayload() - - if err := json.Unmarshal([]byte(p.Text()), &payloadData); nil != err { - t.Log("Could not marshall payload into test struct") - t.Error(err) - } - - testPayloadFieldsPresent(t, p, "tk") - - err := txn.End() - if nil != err { - t.Error(err) - } - - app.ExpectMetrics(t, append([]internal.WantMetric{ - {Name: "Supportability/DistributedTrace/CreatePayload/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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - - payload := txn.CreateDistributedTracePayload() - - testPayloadFieldsPresent(t, payload, - "ty", "ac", "ap", "tr", "ti", "pr", "sa") - - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - - payload := txn.CreateDistributedTracePayload() - testPayloadFieldsPresent(t, payload, - "ty", "ac", "ap", "id", "tr", "ti", "pr", "sa") - - err = txn.End() - if nil != err { - t.Error(err) - } -} - -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"` - } `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 transport { - case TransportHTTP.name: - return TransportHTTP - case TransportHTTPS.name: - return TransportHTTPS - case TransportKafka.name: - return TransportKafka - case TransportJMS.name: - return TransportJMS - case TransportIronMQ.name: - return TransportIronMQ - case TransportAMQP.name: - return TransportAMQP - case TransportQueue.name: - return TransportQueue - case TransportOther.name: - return TransportOther - default: - return TransportUnknown - } -} - -func runDistributedTraceCrossAgentTestcase(tst *testing.T, tc distributedTraceTestcase, extraAsserts func(expectApp, internal.Validator)) { - t := internal.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.AdaptiveSampler = internal.SampleEverything{} - - }, configCallback, tst) - - txn := app.StartTransaction("hello", nil, nil) - if tc.WebTransaction { - txn.SetWebRequest(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.AcceptDistributedTracePayload(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.AcceptDistributedTracePayload(getTransport(tc.TransportType), string(value)) - } - - //call create each time an outbound payload appears in the testcase - for _, expect := range tc.OutboundPayloads { - actual := txn.CreateDistributedTracePayload().Text() - assertTestCaseOutboundPayload(expect, t, actual) - } - - err := txn.End() - if nil != err { - t.Error(err) - } - - // create WantMetrics and assert - 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", "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"}, - } - - 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, actual string) { - type outboundTestcase struct { - Version [2]uint `json:"v"` - Data map[string]interface{} `json:"d"` - } - var actualPayload outboundTestcase - err := json.Unmarshal([]byte(actual), &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) - payload := makePayload(app, nil) - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if err != errInboundPayloadDTDisabled { - t.Fatal("we expected an error with DT disabled", err) - } - err = txn.End() - if nil != err { - t.Error(err) - } - - // 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", nil, nil) - payload := txn.CreateDistributedTracePayload() - if payload.Text() != "" || payload.HTTPSafe() != "" { - t.Error(payload.Text(), payload.HTTPSafe()) - } -} -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", nil, nil) - payload := txn.CreateDistributedTracePayload() - if payload.Text() != "" || payload.HTTPSafe() != "" { - t.Error(payload.Text(), payload.HTTPSafe()) - } -} - -func TestAcceptPayloadAppNotConnected(t *testing.T) { - // Test that an app which isn't connected does not accept distributed - // trace payloads. - app := testApp(nil, enableBetterCAT, t) - payload := testApp(distributedTracingReplyFields, enableBetterCAT, t). - StartTransaction("name", nil, nil). - CreateDistributedTracePayload() - if payload.Text() == "" { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if nil != err { - t.Error(err) - } - txn.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) - payload := testApp(distributedTracingReplyFields, enableBetterCAT, t). - StartTransaction("name", nil, nil). - CreateDistributedTracePayload() - if payload.Text() == "" { - t.Fatal(payload) - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, payload) - if nil != err { - t.Error(err) - } - txn.End() - app.ExpectMetrics(t, backgroundUnknownCaller) -} diff --git a/internal_errors_13_test.go b/internal_errors_13_test.go deleted file mode 100644 index 7df35a227..000000000 --- a/internal_errors_13_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.13 - -package newrelic - -import ( - "fmt" - "testing" - - "github.com/newrelic/go-agent/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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(alpha()) - if nil != err { - t.Error(err) - } - 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) -} diff --git a/internal_errors_stacktrace_test.go b/internal_errors_stacktrace_test.go deleted file mode 100644 index 431ccafe2..000000000 --- a/internal_errors_stacktrace_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.7 - -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. - testcases := []struct { - Error error - ExpectTopFrame string - }{ - {Error: basicError{}, ExpectTopFrame: "internal.GetStackTrace"}, - {Error: withStack{stack: alpha()}, ExpectTopFrame: "alpha"}, - {Error: withStack{stack: nil}, ExpectTopFrame: "internal.GetStackTrace"}, - {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: "internal.GetStackTrace"}, - } - - for idx, tc := range testcases { - data, err := errDataFromError(tc.Error) - 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) - } - } -} diff --git a/internal_errors_test.go b/internal_errors_test.go deleted file mode 100644 index 7a76c7bbb..000000000 --- a/internal_errors_test.go +++ /dev/null @@ -1,669 +0,0 @@ -// 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/internal" -) - -type myError struct{} - -func (e myError) Error() string { return "my msg" } - -func TestNoticeErrorBackground(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.End() - err := txn.NoticeError(myError{}) - if err != errAlreadyEnded { - t.Error(err) - } - 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 } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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 } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if errorsDisabled != err { - t.Error(err) - } - 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) {} - replyfn := func(reply *internal.ConnectReply) { - json.Unmarshal([]byte(`{"agent_config":{"error_collector.enabled":false}}`), reply) - } - app := testApp(replyfn, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if errorsDisabled != err { - t.Error(err) - } - 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 - } - replyfn := func(reply *internal.ConnectReply) { - json.Unmarshal([]byte(`{"agent_config":{"error_collector.enabled":true}}`), reply) - } - app := testApp(replyfn, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if err != nil { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(nil) - if errNilError != err { - t.Error(err) - } - 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 } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(errorWithClass{class: "zap"}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(errorWithClass{class: ""}) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - e := makeErrorWithStackTrace() - err := txn.NoticeError(e) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - e := withStackTrace{trace: nil} - err := txn.NoticeError(e) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - }) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: extraAttributes, - }) - if nil != err { - t.Error(err) - } - 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 } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: extraAttributes, - }) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: extraAttributes, - }) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - if err := txn.AddAttribute("zip", 123); nil != err { - t.Error(err) - } - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: extraAttributes, - }) - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: extraAttributes, - }) - if _, ok := err.(internal.ErrInvalidAttributeType); !ok { - t.Error(err) - } - 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"} - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: map[string]interface{}{ - "zip": "zap", - "IGNORE_ME": 123, - }, - }) - if nil != err { - t.Error(err) - } - 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 <= internal.AttributeErrorLimit; i++ { - attrs[strconv.Itoa(i)] = i - } - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(Error{ - Message: "my msg", - Class: "my class", - Attributes: attrs, - }) - if errTooManyErrorAttributes != err { - t.Error(err) - } - 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) - 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) - } - } -} diff --git a/internal_response_writer.go b/internal_response_writer.go deleted file mode 100644 index b80a57cb4..000000000 --- a/internal_response_writer.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "bufio" - "io" - "net" - "net/http" - - "github.com/newrelic/go-agent/internal" -) - -func (thd *thread) CloseNotify() <-chan bool { - return thd.txn.getWriter().(http.CloseNotifier).CloseNotify() -} -func (thd *thread) Flush() { - thd.txn.getWriter().(http.Flusher).Flush() -} -func (thd *thread) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return thd.txn.getWriter().(http.Hijacker).Hijack() -} -func (thd *thread) ReadFrom(r io.Reader) (int64, error) { - return thd.txn.getWriter().(io.ReaderFrom).ReadFrom(r) -} - -type threadWithExtras interface { - Transaction - internal.AddAgentAttributer - internal.AddAgentSpanAttributer -} - -func upgradeTxn(thd *thread) Transaction { - // Note that thd.txn.getWriter() is not used here. The transaction is - // locked (or under construction) when this function is used. - - // 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 := thd.txn.writer.(http.CloseNotifier); ok { - interfaceSet |= i0 - } - if _, ok := thd.txn.writer.(http.Flusher); ok { - interfaceSet |= i1 - } - if _, ok := thd.txn.writer.(http.Hijacker); ok { - interfaceSet |= i2 - } - if _, ok := thd.txn.writer.(io.ReaderFrom); ok { - interfaceSet |= i3 - } - switch interfaceSet { - default: // No optional interfaces implemented - return struct { - threadWithExtras - }{thd} - case i0: - return struct { - threadWithExtras - http.CloseNotifier - }{thd, thd} - case i1: - return struct { - threadWithExtras - http.Flusher - }{thd, thd} - case i0 | i1: - return struct { - threadWithExtras - http.CloseNotifier - http.Flusher - }{thd, thd, thd} - case i2: - return struct { - threadWithExtras - http.Hijacker - }{thd, thd} - case i0 | i2: - return struct { - threadWithExtras - http.CloseNotifier - http.Hijacker - }{thd, thd, thd} - case i1 | i2: - return struct { - threadWithExtras - http.Flusher - http.Hijacker - }{thd, thd, thd} - case i0 | i1 | i2: - return struct { - threadWithExtras - http.CloseNotifier - http.Flusher - http.Hijacker - }{thd, thd, thd, thd} - case i3: - return struct { - threadWithExtras - io.ReaderFrom - }{thd, thd} - case i0 | i3: - return struct { - threadWithExtras - http.CloseNotifier - io.ReaderFrom - }{thd, thd, thd} - case i1 | i3: - return struct { - threadWithExtras - http.Flusher - io.ReaderFrom - }{thd, thd, thd} - case i0 | i1 | i3: - return struct { - threadWithExtras - http.CloseNotifier - http.Flusher - io.ReaderFrom - }{thd, thd, thd, thd} - case i2 | i3: - return struct { - threadWithExtras - http.Hijacker - io.ReaderFrom - }{thd, thd, thd} - case i0 | i2 | i3: - return struct { - threadWithExtras - http.CloseNotifier - http.Hijacker - io.ReaderFrom - }{thd, thd, thd, thd} - case i1 | i2 | i3: - return struct { - threadWithExtras - http.Flusher - http.Hijacker - io.ReaderFrom - }{thd, thd, thd, thd} - case i0 | i1 | i2 | i3: - return struct { - threadWithExtras - http.CloseNotifier - http.Flusher - http.Hijacker - io.ReaderFrom - }{thd, thd, thd, thd, thd} - } -} diff --git a/internal_response_writer_test.go b/internal_response_writer_test.go deleted file mode 100644 index a5c54a815..000000000 --- a/internal_response_writer_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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", rw, nil) - if v, ok := txn.(http.CloseNotifier); ok { - v.CloseNotify() - } - if v, ok := txn.(http.Flusher); ok { - v.Flush() - } - if v, ok := txn.(http.Hijacker); ok { - v.Hijack() - } - if v, ok := txn.(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", rw, nil) - if _, ok := txn.(http.CloseNotifier); ok { - t.Error("unexpected CloseNotifier method") - } - if _, ok := txn.(http.Flusher); ok { - t.Error("unexpected Flusher method") - } - if _, ok := txn.(http.Hijacker); ok { - t.Error("unexpected Hijacker method") - } - if _, ok := txn.(io.ReaderFrom); ok { - t.Error("unexpected ReaderFrom method") - } -} - -func TestTransactionTwoExtraMethods(t *testing.T) { - app := testApp(nil, nil, t) - rw := &rwTwoExtraMethods{} - txn := app.StartTransaction("hello", rw, nil) - if _, ok := txn.(http.CloseNotifier); ok { - t.Error("unexpected CloseNotifier method") - } - if v, ok := txn.(http.Flusher); ok { - v.Flush() - } - if v, ok := txn.(http.Hijacker); ok { - v.Hijack() - } - if _, ok := txn.(io.ReaderFrom); ok { - t.Error("unexpected ReaderFrom method") - } - if !rw.hijackCalled || - rw.readFromCalled || - !rw.flushCalled || - rw.closeNotifyCalled { - t.Error("wrong methods called", rw) - } -} diff --git a/internal_segment_attributes_test.go b/internal_segment_attributes_test.go deleted file mode 100644 index 422ae20e4..000000000 --- a/internal_segment_attributes_test.go +++ /dev/null @@ -1,509 +0,0 @@ -// 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/internal" -) - -func TestTraceSegments(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.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", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - datastoreSegment.End() - req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) - externalSegment := StartExternalSegment(txn, req) - internal.AddAgentSpanAttribute(txn, internal.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.StackTraceThreshold. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.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", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - datastoreSegment.End() - req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) - externalSegment := StartExternalSegment(txn, req) - internal.AddAgentSpanAttribute(txn, internal.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.SegmentThreshold = 0 - cfg.TransactionTracer.StackTraceThreshold = 1 * time.Hour - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - basicSegment := StartSegment(txn, "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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.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", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - datastoreSegment.End() - req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) - externalSegment := StartExternalSegment(txn, req) - internal.AddAgentSpanAttribute(txn, internal.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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.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", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - datastoreSegment.End() - req, _ := http.NewRequest("GET", "http://example.com?ignore=me", nil) - externalSegment := StartExternalSegment(txn, req) - internal.AddAgentSpanAttribute(txn, internal.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.Attributes.Enabled = false - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.StackTraceThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - } - app := testApp(crossProcessReplyFn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.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, internal.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.TransactionTracer.Segments.Attributes.Enabled = false - cfg.TransactionTracer.SegmentThreshold = 0 - cfg.TransactionTracer.StackTraceThreshold = 0 - cfg.TransactionTracer.Threshold.IsApdexFailing = false - cfg.TransactionTracer.Threshold.Duration = 0 - } - app := testApp(crossProcessReplyFn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - basicSegment := StartSegment(txn, "basic") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - basicSegment.End() - datastoreSegment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "mycollection", - Operation: "myoperation", - ParameterizedQuery: "myquery", - Host: "myhost", - PortPathOrID: "myport", - DatabaseName: "dbname", - QueryParameters: map[string]interface{}{"zap": "zip"}, - } - internal.AddAgentSpanAttribute(txn, internal.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, internal.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, - }, - }, - }, - }}, - }, - }}) -} diff --git a/internal_serverless_test.go b/internal_serverless_test.go deleted file mode 100644 index 12079b3fb..000000000 --- a/internal_serverless_test.go +++ /dev/null @@ -1,304 +0,0 @@ -// 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/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 = "trustkey" - cfg.ServerlessMode.PrimaryAppID = "456" - } - app := testApp(nil, cfgFn, t) - payload := app.StartTransaction("hello", nil, nil).CreateDistributedTracePayload() - txn := app.StartTransaction("hello", nil, nil) - txn.AcceptDistributedTracePayload(TransportHTTP, payload) - 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/DistributedTrace/AcceptPayload/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 = "trustkey" - } - app := testApp(nil, cfgFn, t) - payload := app.StartTransaction("hello", nil, nil).CreateDistributedTracePayload() - txn := app.StartTransaction("hello", nil, nil) - txn.AcceptDistributedTracePayload(TransportHTTP, payload) - 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/DistributedTrace/AcceptPayload/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) - payload := app.StartTransaction("hello", nil, nil).CreateDistributedTracePayload() - txn := app.StartTransaction("hello", nil, nil) - txn.AcceptDistributedTracePayload(TransportHTTP, payload) - 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/DistributedTrace/AcceptPayload/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", nil, nil) - payload := txn.CreateDistributedTracePayload() - if "" != payload.Text() { - t.Error(payload.Text()) - } - nonemptyPayload := func() DistributedTracePayload { - app := testApp(nil, func(cfg *Config) { - cfgFn(cfg) - cfg.ServerlessMode.AccountID = "123" - cfg.ServerlessMode.TrustedAccountKey = "trustkey" - cfg.ServerlessMode.PrimaryAppID = "456" - }, t) - return app.StartTransaction("hello", nil, nil).CreateDistributedTracePayload() - }() - if "" == nonemptyPayload.Text() { - t.Error(nonemptyPayload.Text()) - } - err := txn.AcceptDistributedTracePayload(TransportHTTP, nonemptyPayload) - if err != nil { - t.Error(err) - } - 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 - } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - txn.SetWebRequest(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 - } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - txn.SetWebRequest(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 } - app := testApp(nil, cfgFn, t) - err := app.RecordCustomMetric("myMetric", 123.0) - if err != errMetricServerless { - t.Error(err) - } -} - -func TestServerlessRecordCustomEvent(t *testing.T) { - cfgFn := func(cfg *Config) { cfg.ServerlessMode.Enabled = true } - app := testApp(nil, cfgFn, t) - - attributes := map[string]interface{}{"zip": 1} - err := app.RecordCustomEvent("myType", attributes) - if err != nil { - t.Error(err) - } - app.ExpectCustomEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "type": "myType", - "timestamp": internal.MatchAnything, - }, - UserAttributes: attributes, - }}) - - buf := &bytes.Buffer{} - internal.ServerlessWrite(app, "my-arn", buf) - - _, data, err := internal.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 - } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, nil) - txn.(internal.AddAgentAttributer).AddAgentAttribute(internal.AttributeAWSLambdaARN, "thearn", nil) - txn.End() - - buf := &bytes.Buffer{} - internal.ServerlessWrite(app, "lambda-test-arn", buf) - - metadata, data, err := internal.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 validSampler(s internal.AdaptiveSampler) bool { - _, isSampleEverything := s.(internal.SampleEverything) - _, isSampleNothing := s.(internal.SampleEverything) - return (nil != s) && !isSampleEverything && !isSampleNothing -} - -func TestServerlessConnectReply(t *testing.T) { - cfg := NewConfig("", "") - 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 !validSampler(reply.AdaptiveSampler) { - t.Error(reply.AdaptiveSampler) - } - - // Now test the defaults: - cfg = NewConfig("", "") - 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) - } - if !validSampler(reply.AdaptiveSampler) { - t.Error(reply.AdaptiveSampler) - } -} diff --git a/internal_set_web_request_test.go b/internal_set_web_request_test.go deleted file mode 100644 index 2373f3fba..000000000 --- a/internal_set_web_request_test.go +++ /dev/null @@ -1,325 +0,0 @@ -// 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/internal" -) - -type customRequest struct { - header http.Header - u *url.URL - method string - transport TransportType -} - -func (r customRequest) Header() http.Header { return r.header } -func (r customRequest) URL() *url.URL { return r.u } -func (r customRequest) Method() string { return r.method } -func (r customRequest) Transport() TransportType { return r.transport } - -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("Host", "myhost") - req.Header.Set("Content-Length", "123") - return req - }() - sampleCustomRequest = func() customRequest { - 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("Host", "myhost") - hdr.Set("Content-Length", "123") - return customRequest{ - header: hdr, - u: u, - method: "GET", - transport: TransportHTTP, - } - }() - 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", nil, nil) - err := txn.SetWebRequest(nil) - if err != nil { - t.Error("unexpected error", err) - } - 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 TestSetWebRequestNilPointer(t *testing.T) { - // Test that calling NewWebRequest with a nil pointer is safe and - // returns a nil interface that SetWebRequest handles safely. - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.SetWebRequest(NewWebRequest(nil)) - if err != nil { - t.Error("unexpected error", err) - } - 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 NewWebRequest correctly turns an *http.Request into a - // WebRequest that SetWebRequest uses as expected. - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.SetWebRequest(NewWebRequest(sampleHTTPRequest)) - if err != nil { - t.Error("unexpected error", err) - } - 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: 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 TestSetWebRequestCustomRequest(t *testing.T) { - // Test that a custom type which implements WebRequest is used by - // SetWebRequest as expected. - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.SetWebRequest(sampleCustomRequest) - if err != nil { - t.Error("unexpected error", err) - } - 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: 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", nil, nil) - txn.End() - err := txn.SetWebRequest(sampleCustomRequest) - if err != errAlreadyEnded { - t.Error("incorrect error", err) - } - 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() return value is used as the - // distributed tracing transport if a distributed tracing header is - // found in the WebRequest.Header(). - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - payload := makePayload(app, nil) - // Copy sampleCustomRequest to avoid modifying it since it is used in - // other tests. - req := sampleCustomRequest - req.header = map[string][]string{ - DistributedTracePayloadHeader: {payload.Text()}, - } - txn := app.StartTransaction("hello", nil, nil) - err := txn.SetWebRequest(req) - if nil != err { - t.Error("unexpected error", err) - } - 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/DistributedTrace/AcceptPayload/Success", Scope: "", Forced: true, Data: singleCount}, - }) - app.ExpectTxnEvents(t, []internal.WantEvent{{ - AgentAttributes: map[string]interface{}{ - "request.method": "GET", - "request.uri": "http://www.newrelic.com", - }, - 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, - }, - }}) -} - -type incompleteRequest struct{} - -func (r incompleteRequest) Header() http.Header { return nil } -func (r incompleteRequest) URL() *url.URL { return nil } -func (r incompleteRequest) Method() string { return "" } -func (r incompleteRequest) Transport() TransportType { return TransportUnknown } - -func TestSetWebRequestIncompleteRequest(t *testing.T) { - // Test SetWebRequest will safely handle situations where the request's - // URL() and Header() methods return nil. - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.SetWebRequest(incompleteRequest{}) - if err != nil { - t.Error("unexpected error", err) - } - 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, - }, - }}) -} diff --git a/internal_set_web_response_test.go b/internal_set_web_response_test.go deleted file mode 100644 index 532e9edd8..000000000 --- a/internal_set_web_response_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// 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/internal" -) - -func TestTransactionStartedWithoutResponse(t *testing.T) { - // Test that the http.ResponseWriter methods of the transaction can - // safely be called if a ResponseWriter is not provided. - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.WriteHeader(123) - if hdr := txn.Header(); hdr != nil { - t.Error(hdr) - } - n, err := txn.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}, - Intrinsics: map[string]interface{}{"name": "OtherTransaction/Go/hello"}, - }}) -} - -func TestSetWebResponseNil(t *testing.T) { - // Test that the http.ResponseWriter methods of the transaction can - // safely be called if txn.SetWebResponse(nil) has been called. - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn = txn.SetWebResponse(nil) - txn.WriteHeader(123) - if hdr := txn.Header(); hdr != nil { - t.Error(hdr) - } - n, err := txn.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}, - Intrinsics: map[string]interface{}{"name": "OtherTransaction/Go/hello"}, - }}) -} - -func TestSetWebResponseSuccess(t *testing.T) { - // Test that the http.ResponseWriter methods of the transaction use the - // writer set by SetWebResponse. - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - w := httptest.NewRecorder() - txn = txn.SetWebResponse(w) - txn.WriteHeader(123) - hdr := txn.Header() - hdr.Set("zip", "zap") - body := "should not panic" - n, err := txn.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}, - 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 using Transaction reference returned by SetWebResponse - // properly has the optional methods that the ResponseWriter does. - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - if _, ok := txn.(http.Flusher); ok { - t.Error("should not have Flusher") - } - txn = txn.SetWebResponse(writerWithFlush{}) - if _, ok := txn.(http.Flusher); !ok { - t.Error("should have Flusher now") - } -} diff --git a/internal_slow_queries_test.go b/internal_slow_queries_test.go deleted file mode 100644 index dd1d42ca8..000000000 --- a/internal_slow_queries_test.go +++ /dev/null @@ -1,851 +0,0 @@ -// 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/internal" - "github.com/newrelic/go-agent/internal/crossagent" -) - -func TestSlowQueryBasic(t *testing.T) { - cfgfn := func(cfg *Config) { - cfg.DatastoreTracer.SlowQuery.Threshold = 0 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - replyfn := func(reply *internal.ConnectReply) { - reply.CollectTraces = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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.DatastoreTracer.SlowQuery.Threshold = 0 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - ds := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "users", - Operation: "INSERT", - ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", - } - ds.End() - ds = DatastoreSegment{ - StartTime: StartSegmentNow(txn), - Product: DatastoreMySQL, - Collection: "users", - Operation: "INSERT", - ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)", - } - ds.End() - ds = DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - } - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - params := map[string]interface{}{ - "str": "zap", - "int": 123, - } - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - params := map[string]interface{}{ - "str": "zap", - "int": 123, - } - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - replyfn := func(reply *internal.ConnectReply) { - reply.SecurityPolicies.RecordSQL.SetEnabled(false) - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - params := map[string]interface{}{ - "str": "zap", - "int": 123, - } - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - replyfn := func(reply *internal.ConnectReply) { - reply.SecurityPolicies.RecordSQL.SetEnabled(true) - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - params := map[string]interface{}{ - "str": "zap", - "int": 123, - } - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - params := map[string]interface{}{ - "str": "zap", - "int": 123, - } - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - } - app := testApp(nil, cfgfn, t) - var txn Transaction - var txnURL string - if tc.Input.IsWeb { - txnURL = helloPath - txn = app.StartTransaction("hello", nil, helloRequest) - } else { - txn = app.StartTransaction("hello", nil, nil) - } - ds := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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 - if tc.Input.SystemHostname != "" { - for i := range metrics { - metrics[i].Name = strings.Replace(metrics[i].Name, - tc.Input.SystemHostname, - internal.ThisHost, -1) - } - expectTraceHost = strings.Replace(expectTraceHost, - tc.Input.SystemHostname, - internal.ThisHost, -1) - } - - tt := internal.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 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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, - }, - } - err := s1.End() - if nil == err { - t.Error("error should have been returned") - } - 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}, - }}) -} diff --git a/internal_span_events_test.go b/internal_span_events_test.go deleted file mode 100644 index e571ba20c..000000000 --- a/internal_span_events_test.go +++ /dev/null @@ -1,591 +0,0 @@ -// 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/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.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := StartSegment(txn, "mySegment") - segment.End() - txn.End() - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "priority": internal.MatchAnything, - "guid": "0e97aeb2f79d5d27", - "transactionId": "d9466896a525ccbf", - "nr.entryPoint": true, - "traceId": "d9466896a525ccbf", - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "name": "Custom/mySegment", - "sampled": true, - "category": "generic", - "priority": internal.MatchAnything, - "guid": "bcfb32e050b264b8", - "transactionId": "d9466896a525ccbf", - "traceId": "d9466896a525ccbf", - "parentId": "0e97aeb2f79d5d27", - }, - 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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.SpanEvents.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := StartSegment(txn, "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.AdaptiveSampler = internal.SampleEverything{} - reply.CollectSpanEvents = false - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := StartSegment(txn, "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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := StartSegment(txn, "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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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", - }, - }, - }) -} - -func TestSpanEventAttributesDisabled(t *testing.T) { - // Test that SpanEvents.Attributes.Enabled correctly disables span - // attributes. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.SpanEvents.Attributes.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) -} - -func TestSpanEventAttributesSpecificallyExcluded(t *testing.T) { - // Test that SpanEvents.Attributes.Exclude excludes span attributes. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - 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", nil, nil) - segment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) -} - -func TestSpanEventAttributesExcluded(t *testing.T) { - // Test that Attributes.Exclude excludes span attributes. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - 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", nil, nil) - segment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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{}{}, - }, - }) -} - -func TestSpanEventAttributesLASP(t *testing.T) { - // Test that security policies prevent the capture of the input query - // statement. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.SecurityPolicies.RecordSQL.SetEnabled(false) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - segment := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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", - }, - }, - }) -} - -func TestAddAgentSpanAttribute(t *testing.T) { - // Test that AddAgentSpanAttribute successfully adds attributes to - // spans. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - s := StartSegment(txn, "hi") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSOperation, "secret") - s.End() - txn.End() - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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", - }, - }, - }) -} - -func TestAddAgentSpanAttributeExcluded(t *testing.T) { - // Test that span attributes added by AddAgentSpanAttribute are subject - // to span attribute configuration. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - 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", nil, nil) - s := StartSegment(txn, "hi") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSOperation, "secret") - s.End() - txn.End() - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "name": "Custom/hi", - "sampled": true, - "category": "generic", - "parentId": internal.MatchAnything, - }, - 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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - // Do not panic if there are no active spans! - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRegion, "west") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSRequestID, "123") - internal.AddAgentSpanAttribute(txn, internal.SpanAttributeAWSOperation, "secret") - txn.End() - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "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, internal.SpanAttributeAWSRegion, "west") - internal.AddAgentSpanAttribute(nil, internal.SpanAttributeAWSRequestID, "123") - internal.AddAgentSpanAttribute(nil, internal.SpanAttributeAWSOperation, "secret") -} diff --git a/internal_synthetics_test.go b/internal_synthetics_test.go deleted file mode 100644 index 60c8d4cb0..000000000 --- a/internal_synthetics_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// 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/internal" - "github.com/newrelic/go-agent/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", nil, nil) - 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 } - app := testApp(syntheticsConnectReplyFn, cfgFn, t) - clientTxn := app.StartTransaction( - "helloOldCAT", - nil, - 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", - nil, - 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.CrossApplicationTracer.Enabled = false - } - app := testApp(syntheticsConnectReplyFn, cfgFn, t) - clientTxn := app.StartTransaction( - "helloSynthetics", - nil, - 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, - }}) -} diff --git a/internal_test.go b/internal_test.go deleted file mode 100644 index 9a879414b..000000000 --- a/internal_test.go +++ /dev/null @@ -1,2179 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "encoding/json" - "errors" - "math" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/newrelic/go-agent/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...) -) - -// 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 { - return &compatibleResponseRecorder{ - ResponseRecorder: httptest.NewRecorder(), - } -} - -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(`Host`, `my_domain.com`) - r.Header.Add(`User-Agent`, `Mozilla/5.0`) - r.Header.Add(`Referer`, `http://en.wikipedia.org/zip?secret=password`) - - 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.contentType": "text/html; charset=utf-8", - } -) - -func TestNewApplicationNil(t *testing.T) { - cfg := NewConfig("appname", "wrong length") - cfg.Enabled = false - app, err := NewApplication(cfg) - 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 interface { - internal.Expect - Application -} - -func testApp(replyfn func(*internal.ConnectReply), cfgfn func(*Config), t testing.TB) expectApp { - cfg := NewConfig("my app", testLicenseKey) - - if nil != cfgfn { - cfgfn(&cfg) - } - - // Prevent spawning app goroutines in tests. - if !cfg.ServerlessMode.Enabled { - cfg.Enabled = false - } - - app, err := newApp(cfg) - if nil != err { - t.Fatal(err) - } - - internal.HarvestTesting(app, replyfn) - - return app.(expectApp) -} - -func TestRecordCustomEventSuccess(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomEvent("myType", validParams) - if nil != err { - t.Error(err) - } - 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) - err := app.RecordCustomEvent("myType", validParams) - if err != errHighSecurityEnabled { - t.Error(err) - } - 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) - err := app.RecordCustomEvent("myType", validParams) - if err != errSecurityPolicy { - t.Error(err) - } - app.ExpectCustomEvents(t, []internal.WantEvent{}) -} - -func TestRecordCustomEventEventsDisabled(t *testing.T) { - cfgfn := func(cfg *Config) { cfg.CustomInsightsEvents.Enabled = false } - app := testApp(nil, cfgfn, t) - err := app.RecordCustomEvent("myType", validParams) - if err != errCustomEventsDisabled { - t.Error(err) - } - app.ExpectCustomEvents(t, []internal.WantEvent{}) -} - -func TestRecordCustomEventBadInput(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomEvent("????", validParams) - if err != internal.ErrEventTypeRegex { - t.Error(err) - } - app.ExpectCustomEvents(t, []internal.WantEvent{}) -} - -func TestRecordCustomEventRemoteDisable(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { reply.CollectCustomEvents = false } - app := testApp(replyfn, nil, t) - err := app.RecordCustomEvent("myType", validParams) - if err != errCustomEventsRemoteDisabled { - t.Error(err) - } - app.ExpectCustomEvents(t, []internal.WantEvent{}) -} - -func TestRecordCustomMetricSuccess(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomMetric("myMetric", 123.0) - if nil != err { - t.Error(err) - } - 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) - err := app.RecordCustomMetric("", 123.0) - if err != errMetricNameEmpty { - t.Error(err) - } -} - -func TestRecordCustomMetricNaN(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomMetric("myMetric", math.NaN()) - if err != errMetricNaN { - t.Error(err) - } -} - -func TestRecordCustomMetricPositiveInf(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomMetric("myMetric", math.Inf(0)) - if err != errMetricInf { - t.Error(err) - } -} - -func TestRecordCustomMetricNegativeInf(t *testing.T) { - app := testApp(nil, nil, t) - err := app.RecordCustomMetric("myMetric", math.Inf(-1)) - if err != errMetricInf { - t.Error(err) - } -} - -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", w, nil) - w.header.Add("zip", "zap") - if out := txn.Header(); out.Get("zip") != "zap" { - t.Error(out.Get("zip")) - } - w.written = 123 - if out, _ := txn.Write(nil); out != 123 { - t.Error(out) - } - if txn.WriteHeader(503); w.code != 503 { - t.Error(w.code) - } -} - -func TestTransactionEventWeb(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - err := txn.End() - if nil != err { - t.Error(err) - } - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "WebTransaction/Go/hello", - "nr.apdexPerfZone": "S", - }, - }}) -} - -func TestTransactionEventBackground(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.End() - if nil != err { - t.Error(err) - } - 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 } - app := testApp(nil, cfgFn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - err := txn.End() - if nil != err { - t.Error(err) - } - app.ExpectTxnEvents(t, []internal.WantEvent{}) -} - -func TestTransactionEventRemotelyDisabled(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { reply.CollectAnalyticsEvents = false } - app := testApp(replyfn, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - err := txn.End() - if nil != err { - t.Error(err) - } - app.ExpectTxnEvents(t, []internal.WantEvent{}) -} - -func myErrorHandler(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("my response")) - if txn, ok := w.(Transaction); ok { - txn.NoticeError(myError{}) - } -} - -func TestWrapHandleFunc(t *testing.T) { - app := testApp(nil, nil, t) - mux := http.NewServeMux() - mux.HandleFunc(WrapHandleFunc(app, 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/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: mergeAttributes(helloRequestAttributes, map[string]interface{}{ - "httpResponseCode": "200", - }), - }}) - app.ExpectMetrics(t, webErrorMetrics) -} - -func TestWrapHandle(t *testing.T) { - app := testApp(nil, nil, t) - 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) - } - - 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: mergeAttributes(helloRequestAttributes, map[string]interface{}{ - "httpResponseCode": "200", - }), - }}) - app.ExpectMetrics(t, webErrorMetrics) -} - -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 TestSetName(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("one", nil, nil) - if err := txn.SetName("hello"); nil != err { - t.Error(err) - } - txn.End() - if err := txn.SetName("three"); err != errAlreadyEnded { - t.Error(err) - } - - app.ExpectMetrics(t, backgroundMetrics) -} - -func deferEndPanic(txn Transaction, panicMe interface{}) (r interface{}) { - defer func() { - r = recover() - }() - - defer txn.End() - - panic(panicMe) -} - -func TestPanicError(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - - 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: internal.PanicErrorKlass, - }}) - app.ExpectErrorEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": internal.PanicErrorKlass, - "error.message": "my msg", - "transactionName": "OtherTransaction/Go/hello", - }, - }}) - app.ExpectMetrics(t, backgroundErrorMetrics) -} - -func TestPanicString(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - - 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: internal.PanicErrorKlass, - }}) - app.ExpectErrorEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": internal.PanicErrorKlass, - "error.message": "my string", - "transactionName": "OtherTransaction/Go/hello", - }, - }}) - app.ExpectMetrics(t, backgroundErrorMetrics) -} - -func TestPanicInt(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - - 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: internal.PanicErrorKlass, - }}) - app.ExpectErrorEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "error.class": internal.PanicErrorKlass, - "error.message": "22", - "transactionName": "OtherTransaction/Go/hello", - }, - }}) - app.ExpectMetrics(t, backgroundErrorMetrics) -} - -func TestPanicNil(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - - 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, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - - txn.WriteHeader(http.StatusBadRequest) // 400 - txn.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", - }), - }}) - app.ExpectMetrics(t, webErrorMetrics) -} - -func TestResponseCode404Filtered(t *testing.T) { - app := testApp(nil, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - - txn.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 = - append(cfg.ErrorCollector.IgnoreStatusCodes, 405) - } - app := testApp(nil, cfgFn, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - - txn.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 - } - 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", w, helloRequest) - - txn.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) { - } - 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", w, helloRequest) - - txn.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", - }), - }}) - app.ExpectMetrics(t, webErrorMetrics) -} - -func TestResponseCodeAfterEnd(t *testing.T) { - app := testApp(nil, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - - txn.End() - txn.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, nil, t) - w := newCompatibleResponseRecorder() - txn := app.StartTransaction("hello", w, helloRequest) - - txn.Write([]byte("zap")) - txn.WriteHeader(http.StatusBadRequest) - - txn.End() - - if out := w.Body.String(); "zap" != out { - 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, nil, 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", nil, 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.NoticeError(myError{}) - err := txn.Ignore() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - txn.NoticeError(myError{}) - txn.End() - err := txn.Ignore() - if err != errAlreadyEnded { - t.Error(err) - } - 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 := internal.HostFromURL(u) - if nil != err || nil != u || "" != host { - t.Error(u, err, internal.HostFromURL(u)) - } - // segment only containing url - u, err = externalSegmentURL(&ExternalSegment{URL: rawURL}) - host = internal.HostFromURL(u) - if nil != err || host != "url.com" { - t.Error(u, err, internal.HostFromURL(u)) - } - // segment only containing request - u, err = externalSegmentURL(&ExternalSegment{Request: req}) - host = internal.HostFromURL(u) - if nil != err || "request.com" != host { - t.Error(host) - } - // segment only containing response - u, err = externalSegmentURL(&ExternalSegment{Response: response}) - host = internal.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 = internal.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 = internal.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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - func() { - defer StartSegment(txn, "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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - err := StartSegment(txn, "segment").End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := StartSegment(txn, "s1") - s2 := StartSegment(txn, "s1") - err1 := s1.End() - err2 := s2.End() - if nil != err1 { - t.Error(err1) - } - if nil == err2 { - t.Error(err2) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - txn.End() - s := StartSegment(txn, "segment") - err := s.End() - if err != errAlreadyEnded { - t.Error(err) - } - app.ExpectMetrics(t, webMetrics) -} - -func TestTraceSegmentEndedBeforeEndSegment(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := StartSegment(txn, "segment") - txn.End() - err := s.End() - if err != errAlreadyEnded { - t.Error(err) - } - - app.ExpectMetrics(t, webMetrics) -} - -func TestTraceSegmentPanic(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - func() { - defer func() { - recover() - }() - - func() { - defer StartSegment(txn, "f1").End() - - func() { - t := StartSegment(txn, "f2") - - func() { - defer StartSegment(txn, "f3").End() - - func() { - StartSegment(txn, "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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := Segment{Name: "hello"} - err := s.End() - if err != nil { - t.Error(err) - } - txn.End() - app.ExpectMetrics(t, webMetrics) -} - -func TestTraceDatastore(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := DatastoreSegment{} - s.StartTime = txn.StartSegmentNow() - s.Product = DatastoreMySQL - s.Collection = "my_table" - s.Operation = "SELECT" - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := DatastoreSegment{ - StartTime: txn.StartSegmentNow(), - Product: DatastoreMySQL, - Collection: "my_table", - Operation: "SELECT", - } - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := DatastoreSegment{ - StartTime: txn.StartSegmentNow(), - } - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - var s DatastoreSegment - s.Product = DatastoreMySQL - s.Collection = "my_table" - s.Operation = "SELECT" - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - txn.NoticeError(myError{}) - s := DatastoreSegment{ - StartTime: txn.StartSegmentNow(), - Product: DatastoreMySQL, - Collection: "my_table", - Operation: "SELECT", - } - txn.End() - err := s.End() - if errAlreadyEnded != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: "http://example.com/", - } - err := s.End() - if nil != err { - t.Error(err) - } - 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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: "https://otherhost.com/path/zip/zap?secret=ssshhh", - Host: "bufnet", - Procedure: "TestApplication/DoUnaryUnary", - Library: "grpc", - } - err := s.End() - if nil != err { - t.Error(err) - } - 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{}{ - "name": "WebTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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". - }, - }, - }) -} - -func TestExternalSegmentCustomFieldsWithRequest(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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" - err := s.End() - if nil != err { - t.Error(err) - } - 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{}{ - "name": "WebTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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". - }, - }, - }) -} - -func TestExternalSegmentCustomFieldsWithResponse(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.CrossApplicationTracer.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - req, _ := http.NewRequest("GET", "https://www.something.com/path/zip/zap?secret=ssshhh", nil) - resp := &http.Response{Request: req} - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - Response: resp, - Host: "bufnet", - Procedure: "TestApplication/DoUnaryUnary", - Library: "grpc", - } - err := s.End() - if nil != err { - t.Error(err) - } - 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{}{ - "name": "WebTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - 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". - }, - }, - }) -} - -func TestTraceExternalBadURL(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: ":example.com/", - } - err := s.End() - if nil == err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: "http://example.com/", - } - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - } - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - txn.NoticeError(myError{}) - var s ExternalSegment - err := s.End() - if nil != err { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - txn.NoticeError(myError{}) - s := ExternalSegment{ - StartTime: txn.StartSegmentNow(), - URL: "http://example.com/", - } - txn.End() - err := s.End() - if err != errAlreadyEnded { - t.Error(err) - } - 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 TestRoundTripper(t *testing.T) { - app := testApp(distributedTracingReplyFields, enableBetterCAT, t) - txn := app.StartTransaction("hello", nil, nil) - 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(DistributedTracePayloadHeader) - 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") - }) - client.Transport = NewRoundTripper(txn, 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(DistributedTracePayloadHeader) - 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}, - }, 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, - "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) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - url := "http://example.com/" - 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") - }) - client.Transport = NewRoundTripper(txn, inner) - resp, err := client.Get(url) - 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 TestTraceBelowThreshold(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, helloRequest) - txn.End() - app.ExpectTxnTraces(t, []internal.WantTxnTrace{}) -} - -func TestTraceBelowThresholdBackground(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - 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.SegmentThreshold = 0 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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.SegmentThreshold = 0 - cfg.TransactionTracer.Enabled = false - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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.SegmentThreshold = 0 - } - replyfn := func(reply *internal.ConnectReply) { - json.Unmarshal([]byte(`{"agent_config":{"transaction_tracer.enabled":false}}`), reply) - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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.SegmentThreshold = 0 - cfg.TransactionTracer.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", nil, 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.SegmentThreshold = 0 - cfg.TransactionTracer.Enabled = true - } - 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", nil, 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.SegmentThreshold = 0 - } - replyfn := func(reply *internal.ConnectReply) { - reply.CollectTraces = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, 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.SegmentThreshold = 0 - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := StartSegment(txn, "s1") - s1.End() - s2 := ExternalSegment{ - StartTime: StartSegmentNow(txn), - URL: "http://example.com", - } - s2.End() - s3 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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.SegmentThreshold = 1 * time.Hour - } - app := testApp(nil, cfgfn, t) - txn := app.StartTransaction("hello", nil, helloRequest) - s1 := StartSegment(txn, "s1") - s1.End() - s2 := ExternalSegment{ - StartTime: StartSegmentNow(txn), - URL: "http://example.com", - } - s2.End() - s3 := DatastoreSegment{ - StartTime: StartSegmentNow(txn), - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - err := txn.NoticeError(myError{}) - if nil != err { - t.Error(err) - } - txn.End() - app.ExpectTxnEvents(t, []internal.WantEvent{{ - Intrinsics: map[string]interface{}{ - "name": "OtherTransaction/Go/hello", - "error": true, - }, - }}) -} - -func TestTransactionApplication(t *testing.T) { - txn := testApp(nil, nil, t).StartTransaction("hello", nil, nil) - app := txn.Application() - err := app.RecordCustomMetric("myMetric", 123.0) - if nil != err { - t.Error(err) - } - expectData := []float64{1, 123.0, 123.0, 123.0, 123.0, 123.0 * 123.0} - app.(expectApp).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, nil, t) - txn := app.StartTransaction("hello", flushWriter{}, nil) - if _, ok := txn.(http.Flusher); !ok { - t.Error("transaction should have flush") - } - s1 := StartSegment(txn, "mainThread") - asyncThread := txn.NewGoroutine() - // Test that the async transaction reference has the correct optional - // interface behavior. - if _, ok := asyncThread.(http.Flusher); !ok { - t.Error("async transaction reference should have flush") - } - s2 := StartSegment(asyncThread, "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. - if err := asyncThread.End(); nil != err { - t.Error(err) - } - threadAfterEnd := asyncThread.NewGoroutine() - if _, ok := threadAfterEnd.(http.Flusher); !ok { - t.Error("after end transaction reference should have flush") - } - if err := threadAfterEnd.End(); err != errAlreadyEnded { - t.Error(err) - } - 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.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - s := MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationType: MessageQueue, - DestinationName: "myQueue", - } - err := s.End() - if err != nil { - t.Error(err) - } - 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{}{ - "name": "OtherTransaction/Go/hello", - "sampled": true, - "category": "generic", - "nr.entryPoint": true, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - { - Intrinsics: map[string]interface{}{ - "parentId": internal.MatchAnything, - "name": "MessageBroker/RabbitMQ/Queue/Produce/Named/myQueue", - "category": "generic", - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }, - }) -} - -func TestMessageProducerSegmentMissingDestinationType(t *testing.T) { - app := testApp(nil, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationName: "myQueue", - } - err := s.End() - if err != nil { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationType: MessageQueue, - DestinationTemporary: true, - DestinationName: "myQueue0123456789", - } - err := s.End() - if err != nil { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationType: MessageQueue, - } - err := s.End() - if err != nil { - t.Error(err) - } - 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, nil, t) - txn := app.StartTransaction("hello", nil, nil) - s := MessageProducerSegment{ - StartTime: StartSegmentNow(txn), - Library: "RabbitMQ", - DestinationType: MessageQueue, - DestinationTemporary: true, - DestinationName: "myQueue0123456789", - } - txn.End() - err := s.End() - if err != errAlreadyEnded { - t.Error("expected already ended error", err) - } - 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) { - s := MessageProducerSegment{ - StartTime: StartSegmentNow(nil), - Library: "RabbitMQ", - DestinationType: MessageQueue, - DestinationTemporary: true, - DestinationName: "myQueue0123456789", - } - s.End() -} - -func TestMessageProducerSegmentNilSegment(t *testing.T) { - var s *MessageProducerSegment - s.End() -} diff --git a/internal_txn.go b/internal_txn.go deleted file mode 100644 index 206c77fca..000000000 --- a/internal_txn.go +++ /dev/null @@ -1,1246 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "strings" - "sync" - "time" - - "github.com/newrelic/go-agent/internal" -) - -type txnInput struct { - // This ResponseWriter should only be accessed using txn.getWriter() - writer http.ResponseWriter - app Application - Consumer dataConsumer - *appRun -} - -type txn struct { - txnInput - // 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 - - internal.TxnData - - mainThread internal.Thread - asyncThreads []*internal.Thread -} - -type thread struct { - *txn - // thread does not have locking because it should only be accessed while - // the txn is locked. - thread *internal.Thread -} - -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 *internal.Thread) { - 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 newTxn(input txnInput, name string) *thread { - txn := &txn{ - txnInput: input, - } - txn.markStart(time.Now()) - - txn.Name = name - txn.Attrs = internal.NewAttributes(input.AttributeConfig) - - if input.Config.DistributedTracer.Enabled { - txn.BetterCAT.Enabled = true - txn.BetterCAT.Priority = internal.NewPriority() - txn.TraceIDGenerator = input.Reply.TraceIDGenerator - txn.BetterCAT.ID = txn.TraceIDGenerator.GenerateTraceID() - txn.SpanEventsEnabled = txn.Config.SpanEvents.Enabled - txn.LazilyCalculateSampled = txn.lazilyCalculateSampled - } - - txn.Attrs.Agent.Add(internal.AttributeHostDisplayName, txn.Config.HostDisplayName, nil) - txn.TxnTrace.Enabled = txn.Config.TransactionTracer.Enabled - txn.TxnTrace.SegmentThreshold = txn.Config.TransactionTracer.SegmentThreshold - txn.StackTraceThreshold = txn.Config.TransactionTracer.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, input.Reply) - - return &thread{ - txn: txn, - thread: &txn.mainThread, - } -} - -// 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.Reply.AdaptiveSampler.ComputeSampled(txn.BetterCAT.Priority.Float32(), time.Now()) - if txn.BetterCAT.Sampled { - txn.BetterCAT.Priority += 1.0 - } - txn.sampledCalculated = true - return txn.BetterCAT.Sampled -} - -type requestWrap struct{ request *http.Request } - -func (r requestWrap) Header() http.Header { return r.request.Header } -func (r requestWrap) URL() *url.URL { return r.request.URL } -func (r requestWrap) Method() string { return r.request.Method } - -func (r requestWrap) Transport() TransportType { - if strings.HasPrefix(r.request.Proto, "HTTP") { - if r.request.TLS != nil { - return TransportHTTPS - } - return TransportHTTP - } - return TransportUnknown - -} - -type staticWebRequest struct { - header http.Header - url *url.URL - method string - transport TransportType -} - -func (r staticWebRequest) Header() http.Header { return r.header } -func (r staticWebRequest) URL() *url.URL { return r.url } -func (r staticWebRequest) Method() string { return r.method } -func (r staticWebRequest) Transport() TransportType { return TransportHTTP } - -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 - - if nil == r { - return nil - } - h := r.Header() - if nil != h { - txn.Queuing = internal.QueueDuration(h, txn.Start) - - if p := h.Get(DistributedTracePayloadHeader); p != "" { - txn.acceptDistributedTracePayloadLocked(r.Transport(), p) - } - - txn.CrossProcess.InboundHTTPRequest(h) - } - - internal.RequestAgentAttributes(txn.Attrs, r.Method(), h, r.URL()) - - return nil -} - -func (thd *thread) SetWebResponse(w http.ResponseWriter) Transaction { - txn := thd.txn - txn.Lock() - defer txn.Unlock() - - // Replace the ResponseWriter even if the transaction has ended so that - // consumers calling ResponseWriter methods on the transactions see that - // data flowing through as expected. - txn.writer = w - - return upgradeTxn(&thread{ - thread: thd.thread, - txn: txn, - }) -} - -func (txn *txn) freezeName() { - if txn.ignore || ("" != txn.FinalName) { - return - } - - txn.FinalName = internal.CreateFullTxnName(txn.Name, txn.Reply, txn.IsWeb) - if "" == txn.FinalName { - txn.ignore = true - } -} - -func (txn *txn) getsApdex() bool { - return txn.IsWeb -} - -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 *internal.Harvest) { - - var priority internal.Priority - if txn.BetterCAT.Enabled { - priority = txn.BetterCAT.Priority - } else { - priority = internal.NewPriority() - } - - internal.CreateTxnMetrics(&txn.TxnData, h.Metrics) - internal.MergeBreakdownMetrics(&txn.TxnData, h.Metrics) - - if txn.Config.TransactionEvents.Enabled { - // Allocate a new TxnEvent to prevent a reference to the large transaction. - alloc := new(internal.TxnEvent) - *alloc = txn.TxnData.TxnEvent - h.TxnEvents.AddTxnEvent(alloc, priority) - } - - if txn.Reply.CollectErrors { - internal.MergeTxnErrors(&h.ErrorTraces, txn.Errors, txn.TxnEvent) - } - - if txn.Config.ErrorCollector.CaptureEvents { - for _, e := range txn.Errors { - errEvent := &internal.ErrorEvent{ - ErrorData: *e, - TxnEvent: txn.TxnEvent, - } - // Since the stack trace is not used in error events, remove the reference - // to minimize memory. - errEvent.Stack = nil - h.ErrorEvents.Add(errEvent, priority) - } - } - - if txn.shouldSaveTrace() { - h.TxnTraces.Witness(internal.HarvestTrace{ - TxnEvent: txn.TxnEvent, - Trace: txn.TxnTrace, - }) - } - - if nil != txn.SlowQueries { - h.SlowSQLs.Merge(txn.SlowQueries, txn.TxnEvent) - } - - if txn.BetterCAT.Sampled && txn.SpanEventsEnabled { - h.SpanEvents.MergeFromTransaction(&txn.TxnData) - } -} - -func headersJustWritten(txn *txn, code int, hdr http.Header) { - txn.Lock() - defer txn.Unlock() - - if txn.finished { - return - } - if txn.wroteHeader { - return - } - txn.wroteHeader = true - - internal.ResponseHeaderAttributes(txn.Attrs, hdr) - internal.ResponseCodeAttribute(txn.Attrs, code) - - if txn.appRun.responseCodeIsError(code) { - e := internal.TxnErrorFromResponseCode(time.Now(), code) - e.Stack = internal.GetStackTrace() - txn.noticeErrorInternal(e) - } -} - -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 := internal.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 internal.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) - } - } - } -} - -// getWriter is used to access the transaction's ResponseWriter. The -// ResponseWriter is mutex protected since it may be changed with -// txn.SetWebResponse, and we want changes to be visible across goroutines. The -// ResponseWriter is accessed using this getWriter() function rather than directly -// in mutex protected methods since we do NOT want the transaction to be locked -// while calling the ResponseWriter's methods. -func (txn *txn) getWriter() http.ResponseWriter { - txn.Lock() - rw := txn.writer - txn.Unlock() - return rw -} - -func nilSafeHeader(rw http.ResponseWriter) http.Header { - if nil == rw { - return nil - } - return rw.Header() -} - -func (txn *txn) Header() http.Header { - return nilSafeHeader(txn.getWriter()) -} - -func (txn *txn) Write(b []byte) (n int, err error) { - rw := txn.getWriter() - hdr := nilSafeHeader(rw) - - // This is safe to call unconditionally, even if Write() is called multiple - // times; see also the commentary in addCrossProcessHeaders(). - addCrossProcessHeaders(txn, hdr) - - if rw != nil { - n, err = rw.Write(b) - } - - headersJustWritten(txn, http.StatusOK, hdr) - - return -} - -func (txn *txn) WriteHeader(code int) { - rw := txn.getWriter() - hdr := nilSafeHeader(rw) - - addCrossProcessHeaders(txn, hdr) - - if nil != rw { - rw.WriteHeader(code) - } - - headersJustWritten(txn, code, hdr) -} - -func (thd *thread) End() error { - txn := thd.txn - txn.Lock() - defer txn.Unlock() - - if txn.finished { - return errAlreadyEnded - } - - txn.finished = true - - r := recover() - if nil != r { - e := internal.TxnErrorFromPanic(time.Now(), r) - e.Stack = internal.GetStackTrace() - txn.noticeErrorInternal(e) - } - - 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.Zone = internal.ApdexFailing - } else { - txn.Zone = internal.CalculateApdexZone(txn.ApdexThreshold, txn.Duration) - } - } else { - txn.Zone = internal.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.ignore { - txn.Consumer.Consume(txn.Reply.RunID, txn) - } - - // Note that if a consumer uses `panic(nil)`, the panic will not - // propagate. - if nil != r { - panic(r) - } - - 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 internal.AddUserAttribute(txn.Attrs, name, value, internal.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 (txn *txn) noticeErrorInternal(err internal.ErrorData) error { - if !txn.Config.ErrorCollector.Enabled { - return errorsDisabled - } - - if nil == txn.Errors { - txn.Errors = internal.NewTxnErrors(internal.MaxTxnErrors) - } - - if txn.Config.HighSecurity { - err.Msg = highSecurityErrorMsg - } - - if !txn.Reply.SecurityPolicies.AllowRawExceptionMessages.Enabled() { - err.Msg = securityPolicyErrorMsg - } - - txn.Errors.Add(err) - txn.TxnData.TxnEvent.HasError = true //mark transaction as having an error - return nil -} - -var ( - errTooManyErrorAttributes = fmt.Errorf("too many extra attributes: limit is %d", - internal.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) internal.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) (data internal.ErrorData, err error) { - cause := errorCause(input) - - data = internal.ErrorData{ - When: time.Now(), - Msg: input.Error(), - } - - 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 = internal.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) > internal.AttributeErrorLimit { - err = errTooManyErrorAttributes - return - } - - data.ExtraAttributes = make(map[string]interface{}) - for key, val := range unvetted { - val, err = internal.ValidateUserAttribute(key, val) - if nil != err { - return - } - data.ExtraAttributes[key] = val - } - } - - return data, nil -} - -func (txn *txn) NoticeError(input error) error { - txn.Lock() - defer txn.Unlock() - - if txn.finished { - return errAlreadyEnded - } - - if nil == input { - return errNilError - } - - data, err := errDataFromError(input) - if nil != err { - return err - } - - if txn.Config.HighSecurity || !txn.Reply.SecurityPolicies.CustomParameters.Enabled() { - data.ExtraAttributes = nil - } - - return txn.noticeErrorInternal(data) -} - -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) Ignore() error { - txn.Lock() - defer txn.Unlock() - - if txn.finished { - return errAlreadyEnded - } - txn.ignore = true - return nil -} - -func (thd *thread) StartSegmentNow() SegmentStartTime { - var s internal.SegmentStartTime - txn := thd.txn - txn.Lock() - if !txn.finished { - s = internal.StartSegment(&txn.TxnData, thd.thread, time.Now()) - } - txn.Unlock() - return SegmentStartTime{ - segment: segment{ - 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 := internal.Obfuscate(internal.BrowserAttributes(txn.Attrs), encodingKey) - if err != nil { - return nil, fmt.Errorf("error getting browser attributes: %v", err) - } - - name, err := internal.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.Now().Sub(txn.Start).Nanoseconds() / (1000 * 1000), - ObfuscatedAttributes: attrs, - ErrorBeacon: txn.Reply.ErrorBeacon, - Agent: txn.Reply.JSAgentFile, - }, - }, nil -} - -func createThread(txn *txn) *internal.Thread { - newThread := internal.NewThread(&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 upgradeTxn(thd) - } - return upgradeTxn(&thread{ - thread: createThread(txn), - txn: txn, - }) -} - -type segment struct { - start internal.SegmentStartTime - thread *thread -} - -func endSegment(s *Segment) error { - if nil == s { - return nil - } - thd := s.StartTime.thread - if nil == thd { - return nil - } - txn := thd.txn - var err error - txn.Lock() - if txn.finished { - err = errAlreadyEnded - } else { - err = internal.EndBasicSegment(&txn.TxnData, thd.thread, s.StartTime.start, time.Now(), s.Name) - } - txn.Unlock() - return err -} - -func endDatastore(s *DatastoreSegment) error { - if nil == s { - return nil - } - 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.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 internal.EndDatastoreSegment(internal.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, - }) -} - -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 { - if nil == s { - return nil - } - 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 internal.EndExternalSegment(internal.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), - }) -} - -func endMessage(s *MessageProducerSegment) error { - if nil == s { - return nil - } - 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 internal.EndMessageSegment(internal.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 internal.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 - if p := thd.CreateDistributedTracePayload().HTTPSafe(); "" != p { - hdr.Add(DistributedTracePayloadHeader, p) - return hdr - } - - return hdr -} - -const ( - maxSampledDistributedPayloads = 35 -) - -type shimPayload struct{} - -func (s shimPayload) Text() string { return "" } -func (s shimPayload) HTTPSafe() string { return "" } - -func (thd *thread) CreateDistributedTracePayload() (payload DistributedTracePayload) { - payload = shimPayload{} - - txn := thd.txn - txn.Lock() - defer txn.Unlock() - - if !txn.BetterCAT.Enabled { - return - } - - if txn.finished { - txn.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++ - - var p internal.Payload - p.Type = internal.CallerType - p.Account = txn.Reply.AccountID - - p.App = txn.Reply.PrimaryAppID - p.TracedID = txn.BetterCAT.TraceID() - p.Priority = txn.BetterCAT.Priority - p.Timestamp.Set(time.Now()) - p.TransactionID = txn.BetterCAT.ID // Set the transaction ID to the transaction guid. - - if txn.Reply.AccountID != txn.Reply.TrustedAccountKey { - p.TrustedAccountKey = txn.Reply.TrustedAccountKey - } - - sampled := txn.lazilyCalculateSampled() - if sampled && txn.SpanEventsEnabled { - p.ID = txn.CurrentSpanIdentifier(thd.thread) - } - - // 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) - } - - txn.CreatePayloadSuccess = true - - payload = p - return -} - -var ( - errOutboundPayloadCreated = errors.New("outbound payload already created") - errAlreadyAccepted = errors.New("AcceptDistributedTracePayload 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) AcceptDistributedTracePayload(t TransportType, p interface{}) error { - txn.Lock() - defer txn.Unlock() - - return txn.acceptDistributedTracePayloadLocked(t, p) -} - -func (txn *txn) acceptDistributedTracePayloadLocked(t TransportType, p interface{}) error { - - if !txn.BetterCAT.Enabled { - return errInboundPayloadDTDisabled - } - - if txn.finished { - txn.AcceptPayloadException = true - return errAlreadyEnded - } - - if txn.numPayloadsCreated > 0 { - txn.AcceptPayloadCreateBeforeAccept = true - return errOutboundPayloadCreated - } - - if txn.BetterCAT.Inbound != nil { - txn.AcceptPayloadIgnoredMultiple = true - return errAlreadyAccepted - } - - if nil == p { - txn.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 - } - - payload, err := internal.AcceptPayload(p) - if nil != err { - if _, ok := err.(internal.ErrPayloadParse); ok { - txn.AcceptPayloadParseException = true - } else if _, ok := err.(internal.ErrUnsupportedPayloadVersion); ok { - txn.AcceptPayloadIgnoredVersion = true - } else if _, ok := err.(internal.ErrPayloadMissingField); ok { - txn.AcceptPayloadParseException = true - } else { - txn.AcceptPayloadException = true - } - return err - } - - if nil == payload { - return nil - } - - // now that we have a parsed and alloc'd payload, - // let's make sure it has the correct fields - if err := payload.IsValid(); nil != err { - txn.AcceptPayloadParseException = true - return err - } - - // and let's also do our trustedKey check - receivedTrustKey := payload.TrustedAccountKey - if "" == receivedTrustKey { - receivedTrustKey = payload.Account - } - if receivedTrustKey != txn.Reply.TrustedAccountKey { - txn.AcceptPayloadUntrustedAccount = true - return errTrustedAccountKey - } - - if 0 != payload.Priority { - 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 - - // TransportType's name field is not mutable outside of its package - // so the only check needed is if the caller is using an empty TransportType - txn.BetterCAT.Inbound.TransportType = t.name - if t.name == "" { - txn.BetterCAT.Inbound.TransportType = TransportUnknown.name - txn.Config.Logger.Debug("Invalid transport type, defaulting to Unknown", map[string]interface{}{}) - } - - if tm := payload.Timestamp.Time(); txn.Start.After(tm) { - txn.BetterCAT.Inbound.TransportDuration = txn.Start.Sub(tm) - } - - txn.AcceptPayloadSuccess = true - - return nil -} - -func (txn *txn) Application() Application { - return txn.app -} - -func (thd *thread) AddAgentSpanAttribute(key internal.SpanAttribute, val string) { - thd.thread.AddAgentSpanAttribute(key, val) -} - -var ( - // Ensure that txn implements AddAgentAttributer to avoid breaking - // integration package type assertions. - _ internal.AddAgentAttributer = &txn{} -) - -func (txn *txn) AddAgentAttribute(id internal.AgentAttributeID, stringVal string, otherVal interface{}) { - txn.Lock() - defer txn.Unlock() - - if txn.finished { - return - } - txn.Attrs.Agent.Add(id, 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.SpanEventsEnabled && txn.lazilyCalculateSampled() { - 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 = internal.ThisHost - - 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() -} diff --git a/internal_txn_test.go b/internal_txn_test.go deleted file mode 100644 index 227c9dd46..000000000 --- a/internal_txn_test.go +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "testing" - "time" - - "github.com/newrelic/go-agent/internal" - "github.com/newrelic/go-agent/internal/cat" - "github.com/newrelic/go-agent/internal/sysinfo" -) - -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 := NewConfig("my app", "0123456789012345678901234567890123456789") - 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(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.appRun = &appRun{} - tx.BetterCAT.Priority = 0.5 - tx.sampledCalculated = false - tx.BetterCAT.Enabled = true - tx.Reply = &internal.ConnectReply{ - AdaptiveSampler: internal.SampleEverything{}, - } - 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.AdaptiveSampler = internal.SampleNothing{} - 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.appRun = &appRun{} - tx.BetterCAT.Priority = 0.5 - tx.sampledCalculated = false - tx.BetterCAT.Enabled = true - tx.Reply = &internal.ConnectReply{ - AdaptiveSampler: internal.SampleNothing{}, - } - 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.AdaptiveSampler = internal.SampleEverything{} - 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{ - AdaptiveSampler: internal.SampleEverything{}, - } - 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 := internal.StartSegment(&tx.TxnData, &tx.mainThread, start.Add(1*time.Second)) - internal.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 = internal.StartSegment(&tx.TxnData, &tx.mainThread, start.Add(1*time.Second)) - internal.EndBasicSegment(&tx.TxnData, &tx.mainThread, segmentStart, start.Add(2*time.Second), "name") - asyncThread := createThread(tx) - asyncSegmentStart := internal.StartSegment(&tx.TxnData, asyncThread, start.Add(1*time.Second)) - internal.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 = internal.StartSegment(&tx.TxnData, &tx.mainThread, start.Add(1*time.Second)) - internal.EndBasicSegment(&tx.TxnData, &tx.mainThread, segmentStart, start.Add(2*time.Second), "name") - asyncThread = createThread(tx) - asyncSegmentStart = internal.StartSegment(&tx.TxnData, asyncThread, start.Add(1*time.Second)) - internal.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 = internal.StartSegment(&tx.TxnData, &tx.mainThread, start.Add(0*time.Second)) - internal.EndBasicSegment(&tx.TxnData, &tx.mainThread, segmentStart, start.Add(1*time.Second), "name") - asyncThread = createThread(tx) - asyncSegmentStart = internal.StartSegment(&tx.TxnData, asyncThread, start.Add(2*time.Second)) - internal.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, - }) -} - -func TestGetTraceMetadataDistributedTracingDisabled(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "" { - t.Error(metadata.TraceID) - } -} - -func TestGetTraceMetadataSuccess(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "bcfb32e050b264b8" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "d9466896a525ccbf" { - t.Error(metadata.TraceID) - } - StartSegment(txn, "name") - // Span id should be different now that a segment has started. - metadata = txn.GetTraceMetadata() - if metadata.SpanID != "0e97aeb2f79d5d27" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "d9466896a525ccbf" { - t.Error(metadata.TraceID) - } -} - -func TestGetTraceMetadataEnded(t *testing.T) { - // Test that GetTraceMetadata returns empty strings if the transaction - // has been finished. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - txn.End() - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "" { - t.Error(metadata.TraceID) - } -} - -func TestGetTraceMetadataNotSampled(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleNothing{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "d9466896a525ccbf" { - t.Error(metadata.TraceID) - } -} - -func TestGetTraceMetadataSpanEventsDisabled(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - cfg.SpanEvents.Enabled = false - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "d9466896a525ccbf" { - t.Error(metadata.TraceID) - } -} - -func TestGetTraceMetadataInboundPayload(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - reply.TraceIDGenerator = internal.NewTraceIDGenerator(12345) - reply.AccountID = "account-id" - reply.TrustedAccountKey = "trust-key" - reply.PrimaryAppID = "app-id" - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - payload := app.StartTransaction("hello", nil, nil).CreateDistributedTracePayload() - p := payload.(internal.Payload) - p.TracedID = "trace-id" - - txn := app.StartTransaction("hello", nil, nil) - err := txn.AcceptDistributedTracePayload(TransportHTTP, p) - if nil != err { - t.Error(err) - } - metadata := txn.GetTraceMetadata() - if metadata.SpanID != "9d2c19bd03daf755" { - t.Error(metadata.SpanID) - } - if metadata.TraceID != "trace-id" { - t.Error(metadata.TraceID) - } -} - -func TestGetLinkingMetadata(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - 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", nil, nil) - - metadata := txn.GetLinkingMetadata() - host, _ := sysinfo.Hostname() - if metadata.TraceID != "d9466896a525ccbf" { - t.Error("wrong TraceID:", metadata.TraceID) - } - if metadata.SpanID != "bcfb32e050b264b8" { - 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", nil, nil) - - 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) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleNothing{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - sampled := txn.IsSampled() - if sampled == true { - t.Error("txn should not be sampled") - } -} - -func TestIsSampledTrue(t *testing.T) { - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - 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. - replyfn := func(reply *internal.ConnectReply) { - reply.AdaptiveSampler = internal.SampleEverything{} - } - cfgfn := func(cfg *Config) { - cfg.DistributedTracer.Enabled = true - } - app := testApp(replyfn, cfgfn, t) - txn := app.StartTransaction("hello", nil, nil) - txn.End() - sampled := txn.IsSampled() - if sampled == true { - t.Error("finished txn should not be sampled") - } -} diff --git a/log.go b/log.go deleted file mode 100644 index d75a8d079..000000000 --- a/log.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "io" - - "github.com/newrelic/go-agent/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 and logxi are supported by the integration packages -// https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogrus and -// https://godoc.org/github.com/newrelic/go-agent/_integrations/nrlogxi/v1. -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. -func NewLogger(w io.Writer) Logger { - return logger.New(w, false) -} - -// NewDebugLogger creates a basic Logger at debug level. -func NewDebugLogger(w io.Writer) Logger { - return logger.New(w, true) -} diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 000000000..4e0b68c4c --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# run_tests.sh +export PATH=$PATH:/usr/local/go/bin +# Test directory is passed in as an argument +TEST_DIR=$1 +COVERAGE_DIR=$2 +COVERAGE_FILE="$COVERAGE_DIR/coverage.out" + +echo "Coverage profile will be created at $COVERAGE_FILE" + +# 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=. -coverprofile="$COVERAGE_FILE" -covermode=atomic -coverpkg=./... ./... +go vet ./... +verify_go_fmt + +# Remove sql_driver_optional_methods from coverage.out file if it exists +sed -i '/sql_driver_optional_methods/d' "$COVERAGE_FILE" + +## CodeCov Uploader +curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --import # One-time step +curl -Os https://uploader.codecov.io/latest/linux/codecov +curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM +curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig +gpg --verify codecov.SHA256SUM.sig codecov.SHA256SUM +shasum -a 256 -c codecov.SHA256SUM +chmod +x codecov +./codecov -t ${CODECOV_TOKEN} -f "$COVERAGE_FILE" -B "${GITHUB_HEAD_REF:-$GITHUB_REF}" \ No newline at end of file diff --git a/segments.go b/segments.go deleted file mode 100644 index 5fc8ad454..000000000 --- a/segments.go +++ /dev/null @@ -1,203 +0,0 @@ -// 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{ segment } - -// Segment is used to instrument functions, methods, and blocks of code. The -// easiest way use Segment is the StartSegment function. -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 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 - // 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 -} - -// 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 -} - -// 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" -) - -// End finishes the segment. -func (s *Segment) End() error { return endSegment(s) } - -// End finishes the datastore segment. -func (s *DatastoreSegment) End() error { return endDatastore(s) } - -// End finishes the external segment. -func (s *ExternalSegment) End() error { return endExternal(s) } - -// End finishes the message segment. -func (s *MessageProducerSegment) End() error { return endMessage(s) } - -// OutboundHeaders returns the headers that should be attached to the external -// request. -func (s *ExternalSegment) OutboundHeaders() http.Header { - return outboundHeaders(s) -} - -// StartSegmentNow starts timing a segment. This function is recommended over -// Transaction.StartSegmentNow() because it is nil safe. -func StartSegmentNow(txn Transaction) SegmentStartTime { - if nil != txn { - return txn.StartSegmentNow() - } - return SegmentStartTime{} -} - -// StartSegment makes it easy to instrument segments. To time a function, do -// the following: -// -// func timeMe(txn newrelic.Transaction) { -// defer newrelic.StartSegment(txn, "timeMe").End() -// // ... function code here ... -// } -// -// To time a block of code, do the following: -// -// segment := StartSegment(txn, "myBlock") -// // ... code you want to time here ... -// segment.End() -// -func StartSegment(txn Transaction, name string) *Segment { - return &Segment{ - StartTime: StartSegmentNow(txn), - 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: StartSegmentNow(txn), - Request: request, - } - - if request != nil && request.Header != nil { - for key, values := range s.OutboundHeaders() { - for _, value := range values { - request.Header.Add(key, value) - } - } - } - - return s -} diff --git a/sql_driver.go b/sql_driver.go deleted file mode 100644 index 2d923edad..000000000 --- a/sql_driver.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.10 - -package newrelic - -import ( - "context" - "database/sql/driver" -) - -// 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/_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/_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/_integrations/nrmysql/nrmysql.go -// for example use. -func InstrumentSQLConnector(connector driver.Connector, bld SQLDriverSegmentBuilder) driver.Connector { - return &wrapConnector{original: connector, bld: bld} -} - -func (bld SQLDriverSegmentBuilder) useDSN(dsn string) SQLDriverSegmentBuilder { - if f := bld.ParseDSN; nil != f { - f(&bld.BaseSegment, dsn) - } - return bld -} - -func (bld SQLDriverSegmentBuilder) useQuery(query string) SQLDriverSegmentBuilder { - if f := bld.ParseQuery; nil != f { - f(&bld.BaseSegment, query) - } - return bld -} - -func (bld SQLDriverSegmentBuilder) startSegment(ctx context.Context) DatastoreSegment { - segment := bld.BaseSegment - segment.StartTime = StartSegmentNow(FromContext(ctx)) - 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 nil != err { - 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 nil != err { - 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) - 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) - 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) { - return w.original.(driver.Execer).Exec(query, args) -} - -// ExecContext implements ExecerContext. -func (w *wrapConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { - segment := w.bld.useQuery(query).startSegment(ctx) - result, err := w.original.(driver.ExecerContext).ExecContext(ctx, query, args) - if err != driver.ErrSkip { - segment.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) { - return w.original.(driver.Queryer).Query(query, args) -} - -// QueryContext implements QueryerContext. -func (w *wrapConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { - segment := w.bld.useQuery(query).startSegment(ctx) - rows, err := w.original.(driver.QueryerContext).QueryContext(ctx, query, args) - if err != driver.ErrSkip { - segment.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) { - return w.original.Exec(args) -} - -func (w *wrapStmt) Query(args []driver.Value) (driver.Rows, error) { - return w.original.Query(args) -} - -// 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) { - 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) { - 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{} -) diff --git a/sql_driver_optional_methods.go b/sql_driver_optional_methods.go deleted file mode 100644 index 2c9d173c7..000000000 --- a/sql_driver_optional_methods.go +++ /dev/null @@ -1,2243 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +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 - ) - 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 - } - 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} - } -} diff --git a/sql_driver_test.go b/sql_driver_test.go deleted file mode 100644 index fd79ab187..000000000 --- a/sql_driver_test.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// +build go1.10 - -package newrelic - -import ( - "context" - "database/sql/driver" - "strings" - "testing" - - "github.com/newrelic/go-agent/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, nil, t) - dr := InstrumentSQLDriver(testDriver{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - 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, nil, t) - dr := InstrumentSQLDriver(testDriver{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - conn, _ := dr.Open("myhost,myport,mydatabase") - stmt, _ := conn.(driver.ConnPrepareContext).PrepareContext(context.Background(), "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, nil, t) - dr := InstrumentSQLDriver(testDriver{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - 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, nil, t) - dr := InstrumentSQLDriver(testDriver{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - 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, nil, t) - dr := InstrumentSQLDriver(testDriver{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - connector, _ := dr.(driver.DriverContext).OpenConnector("myhost,myport,mydatabase") - conn, _ := connector.Connect(context.Background()) - 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, nil, t) - bld := testBuilder - bld.BaseSegment.Host = "myhost" - bld.BaseSegment.PortPathOrID = "myport" - bld.BaseSegment.DatabaseName = "mydatabase" - connector := InstrumentSQLConnector(testConnector{}, bld) - txn := app.StartTransaction("hello", nil, nil) - conn, _ := connector.Connect(context.Background()) - 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, nil, t) - connector := InstrumentSQLConnector(testConnector{}, testBuilder) - txn := app.StartTransaction("hello", nil, nil) - 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) -} diff --git a/transaction.go b/transaction.go deleted file mode 100644 index 63e8d9916..000000000 --- a/transaction.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "net/http" - "net/url" -) - -// Transaction instruments one logical unit of work: either an inbound web -// request or background task. Start a new Transaction with the -// Application.StartTransaction() method. -type Transaction interface { - // The transaction's http.ResponseWriter methods delegate to the - // http.ResponseWriter provided as a parameter to - // Application.StartTransaction or Transaction.SetWebResponse. This - // allows instrumentation of the response code and response headers. - // These methods may be called safely if the transaction does not have a - // http.ResponseWriter. - http.ResponseWriter - - // 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. - End() error - - // Ignore prevents this transaction's data from being recorded. - Ignore() error - - // SetName names the transaction. Use a limited set of unique names to - // ensure that Transactions are grouped usefully. - SetName(name string) error - - // 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 above - // 400 or below 100 that is not in the IgnoreStatusCodes configuration - // list. This method is unaffected by the IgnoreStatusCodes - // configuration list. - NoticeError(err error) error - - // 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 - AddAttribute(key string, value interface{}) error - - // SetWebRequest marks the transaction as a web transaction. If - // WebRequest is non-nil, SetWebRequest will additionally collect - // details on request attributes, url, and method. If headers are - // present, the agent will look for a distributed tracing header. Use - // NewWebRequest to transform a *http.Request into a WebRequest. - SetWebRequest(WebRequest) error - - // SetWebResponse sets transaction's http.ResponseWriter. After calling - // this method, the transaction may be used in place of the - // ResponseWriter to intercept the response code. This method is useful - // when the 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. - SetWebResponse(http.ResponseWriter) Transaction - - // StartSegmentNow starts timing a segment. The SegmentStartTime - // returned can be used as the StartTime field in Segment, - // DatastoreSegment, or ExternalSegment. We recommend using the - // StartSegmentNow function instead of this method since it checks if - // the Transaction is nil. - StartSegmentNow() SegmentStartTime - - // CreateDistributedTracePayload creates a payload used to link - // transactions. CreateDistributedTracePayload should be called every - // time an outbound call is made since the payload contains a timestamp. - // - // StartExternalSegment calls CreateDistributedTracePayload, so you - // don't need to use it for outbound HTTP calls: Just use - // StartExternalSegment! - // - // This method never returns nil. If the application is disabled or not - // yet connected then this method returns a shim implementation whose - // methods return empty strings. - CreateDistributedTracePayload() DistributedTracePayload - - // AcceptDistributedTracePayload links transactions by accepting a - // distributed trace payload from another transaction. - // - // Application.StartTransaction calls this method automatically if a - // payload is present in the request headers. Therefore, this method - // does not need to be used for typical HTTP transactions. - // - // AcceptDistributedTracePayload should be used as early in the - // transaction as possible. It may not be called after a call to - // CreateDistributedTracePayload. - // - // The payload parameter may be a DistributedTracePayload, a string, or - // a []byte. - AcceptDistributedTracePayload(t TransportType, payload interface{}) error - - // Application returns the Application which started the transaction. - Application() 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. - BrowserTimingHeader() (*BrowserTimingHeader, error) - - // 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. - // - // Example passing a new Transaction reference directly to another - // goroutine: - // - // 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: - // - // ch := make(chan newrelic.Transaction) - // go func() { - // txn := <-ch - // defer newrelic.StartSegment(txn, "async").End() - // time.Sleep(100 * time.Millisecond) - // }() - // ch <- txn.NewGoroutine() - // - NewGoroutine() Transaction - - // GetTraceMetadata returns distributed tracing identifiers. Empty - // string identifiers are returned if the transaction has finished. - GetTraceMetadata() TraceMetadata - - // GetLinkingMetadata returns the fields needed to link data to a trace or - // entity. - GetLinkingMetadata() LinkingMetadata - - // 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. - IsSampled() bool -} - -// DistributedTracePayload traces requests between applications or processes. -// DistributedTracePayloads are automatically added to HTTP requests by -// StartExternalSegment, so you only need to use this if you are tracing through -// a message queue or another non-HTTP communication library. The -// DistributedTracePayload may be marshalled in one of two formats: HTTPSafe or -// Text. All New Relic agents can accept payloads in either format. -type DistributedTracePayload interface { - // HTTPSafe serializes the payload into a string containing http safe - // characters. - HTTPSafe() string - // Text serializes the payload into a string. The format is slightly - // more compact than HTTPSafe. - Text() string -} - -const ( - // DistributedTracePayloadHeader is the header used by New Relic agents - // for automatic trace payload instrumentation. - DistributedTracePayloadHeader = "Newrelic" -) - -// TransportType is used in Transaction.AcceptDistributedTracePayload() to -// represent the type of connection that the trace payload was transported over. -type TransportType struct{ name string } - -// TransportType names used across New Relic agents: -var ( - TransportUnknown = TransportType{name: "Unknown"} - TransportHTTP = TransportType{name: "HTTP"} - TransportHTTPS = TransportType{name: "HTTPS"} - TransportKafka = TransportType{name: "Kafka"} - TransportJMS = TransportType{name: "JMS"} - TransportIronMQ = TransportType{name: "IronMQ"} - TransportAMQP = TransportType{name: "AMQP"} - TransportQueue = TransportType{name: "Queue"} - TransportOther = TransportType{name: "Other"} -) - -// WebRequest may be implemented to provide request information to -// Transaction.SetWebRequest. -type WebRequest interface { - // Header may return nil if you don't have any headers or don't want to - // transform them to http.Header format. - Header() http.Header - // URL may return nil if you don't have a URL or don't want to transform - // it to *url.URL. - URL() *url.URL - Method() string - // If a distributed tracing header is found in the headers returned by - // Header(), this TransportType will be used in the distributed tracing - // metrics. - Transport() TransportType -} - -// NewWebRequest turns a *http.Request into a WebRequest for input into -// Transaction.SetWebRequest. -func NewWebRequest(request *http.Request) WebRequest { - if nil == request { - return nil - } - return requestWrap{request: request} -} - -// NewStaticWebRequest takes the minimum necessary information and creates a static WebRequest out of it -func NewStaticWebRequest(hdrs http.Header, url *url.URL, method string, transport TransportType) WebRequest { - return staticWebRequest{hdrs, url, method, transport} -} - -// LinkingMetadata is returned by Transaction.GetLinkingMetadata. It contains -// identifiers needed 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 newrelic.Config. If - // multiple application names are specified, 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 -} diff --git a/v3/build-script.sh b/v3/build-script.sh deleted file mode 100755 index a31526afb..000000000 --- a/v3/build-script.sh +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2020 New Relic Corporation. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -x -set -e - -# inputs -# 1: repo pin; example: github.com/rewrelic/go-agent@v1.9.0 -pin_go_dependency() { - if [[ ! -z "$1" ]]; then - echo "Pinning: $1" - repo=$(echo "$1" | cut -d '@' -f1) - pinTo=$(echo "$1" | cut -d '@' -f2) - set +e - go get -u "$repo" # this go get will fail to build - set -e - cd "$GOPATH"/src/"$repo" - git checkout "$pinTo" - cd - - fi -} - -verify_go_fmt() { - needsFMT=$(gofmt -d .) - if [ ! -z "$needsFMT" ]; then - echo "$needsFMT" - echo "Please format your code with \"gofmt .\"" - # exit 1 - fi -} - -pwd=$(pwd) -version=$(go version) -echo $version - -tmp=$(echo $version | cut -d 'o' -f4) -shortVersion=${tmp%.*} - -IFS="," -for dir in $DIRS; do - cd "$pwd/$dir" - - # replace go-agent with local pull - go mod edit -replace github.com/newrelic/go-agent/v3="$pwd"/v3 - - # manage dependencies - go mod tidy -go=$shortVersion -compat=$shortVersion - pin_go_dependency "$PIN" - - # run tests - go test -race -benchtime=1ms -bench=. ./... - go vet ./... - verify_go_fmt - - # Test again against the latest version of the dependencies to ensure that - # our instrumentation is up to date. TODO: Perhaps it is possible to - # upgrade all go.mod dependencies to latest master with a go command. - if [ -n "$EXTRATESTING" ]; then - eval "$EXTRATESTING" - go test -race -benchtime=1ms -bench=. ./... - fi -done diff --git a/v3/examples/server/Dockerfile b/v3/examples/server/Dockerfile new file mode 100644 index 000000000..42c4a0d13 --- /dev/null +++ b/v3/examples/server/Dockerfile @@ -0,0 +1,51 @@ +# 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.22 +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 +# diff --git a/v3/examples/server/main.go b/v3/examples/server/main.go index c012e349b..856ed6cfc 100644 --- a/v3/examples/server/main.go +++ b/v3/examples/server/main.go @@ -159,7 +159,7 @@ func message(w http.ResponseWriter, r *http.Request) { func external(w http.ResponseWriter, r *http.Request) { txn := newrelic.FromContext(r.Context()) - req, _ := http.NewRequest("GET", "http://example.com", nil) + 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 @@ -193,7 +193,7 @@ func roundtripper(w http.ResponseWriter, r *http.Request) { client := &http.Client{} client.Transport = newrelic.NewRoundTripper(client.Transport) - request, _ := http.NewRequest("GET", "http://example.com", nil) + 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. diff --git a/v3/go.mod b/v3/go.mod index 4eaf02fb4..afbca8df0 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -1,8 +1,13 @@ module github.com/newrelic/go-agent/v3 -go 1.17 +go 1.20 require ( - github.com/golang/protobuf v1.5.2 - google.golang.org/grpc v1.49.0 + github.com/golang/protobuf v1.5.3 + google.golang.org/grpc v1.56.3 ) + + +retract v3.22.0 // release process error corrected in v3.22.1 + +retract v3.25.0 // release process error corrected in v3.25.1 diff --git a/v3/integrations/logcontext-v2/logWriter/go.mod b/v3/integrations/logcontext-v2/logWriter/go.mod index 7d7910322..cadf5f5cc 100644 --- a/v3/integrations/logcontext-v2/logWriter/go.mod +++ b/v3/integrations/logcontext-v2/logWriter/go.mod @@ -1,8 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/logWriter -go 1.17 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.19.1 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.0 ) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrlogrus/go.mod b/v3/integrations/logcontext-v2/nrlogrus/go.mod index b547c3ce2..c31af8d84 100644 --- a/v3/integrations/logcontext-v2/nrlogrus/go.mod +++ b/v3/integrations/logcontext-v2/nrlogrus/go.mod @@ -1,8 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus -go 1.17 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.18.0 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/sirupsen/logrus v1.8.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrslog/example/main.go b/v3/integrations/logcontext-v2/nrslog/example/main.go new file mode 100644 index 000000000..247d018d4 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/example/main.go @@ -0,0 +1,45 @@ +package 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) + log := slog.New(nrslog.TextHandler(app, os.Stdout, &slog.HandlerOptions{})) + + 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) +} diff --git a/v3/integrations/logcontext-v2/nrslog/go.mod b/v3/integrations/logcontext-v2/nrslog/go.mod new file mode 100644 index 000000000..763031b83 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/go.mod @@ -0,0 +1,8 @@ +module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog + +go 1.20 + +require github.com/newrelic/go-agent/v3 v3.33.1 + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go new file mode 100644 index 000000000..9c476910f --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -0,0 +1,231 @@ +package nrslog + +import ( + "context" + "io" + "log/slog" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +// NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context +type NRHandler struct { + handler slog.Handler + w *LogWriter + app *newrelic.Application + txn *newrelic.Transaction +} + +// 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. +func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) NRHandler { + nrWriter := NewWriter(w, app) + textHandler := slog.NewTextHandler(nrWriter, opts) + wrappedHandler := WrapHandler(app, textHandler) + wrappedHandler.addWriter(&nrWriter) + return wrappedHandler +} + +// 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. +func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) NRHandler { + nrWriter := NewWriter(w, app) + jsonHandler := slog.NewJSONHandler(nrWriter, opts) + wrappedHandler := WrapHandler(app, jsonHandler) + wrappedHandler.addWriter(&nrWriter) + return wrappedHandler +} + +// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction. +// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. +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 + } +} + +// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction it its found +// in a context. +// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. +func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { + if ctx == nil { + return logger + } + + txn := newrelic.FromContext(ctx) + return WithTransaction(txn, logger) +} + +// 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 WrapHandler(app *newrelic.Application, handler slog.Handler) NRHandler { + return NRHandler{ + handler: handler, + app: app, + } +} + +// addWriter is an internal helper function to append an io.Writer to the NRHandler object +func (h *NRHandler) addWriter(w *LogWriter) { + h.w = w +} + +// 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 { + handler := NRHandler{ + handler: h.handler, + app: h.app, + txn: txn, + } + + if h.w != nil { + writer := h.w.WithTransaction(txn) + handler.addWriter(&writer) + } + + return handler +} + +// Enabled reports whether the handler handles records at the given level. +// The handler ignores records whose level is lower. +// It is called early, before any arguments are processed, +// to save effort if the log event should be discarded. +// If called from a Logger method, the first argument is the context +// passed to that method, or context.Background() if nil was passed +// or the method does not take a context. +// The context is passed so Enabled can use its values +// to make a decision. +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. +// The Context argument is as for Enabled. +// It is present solely to provide Handlers access to the context's values. +// Canceling the context should not affect record processing. +// (Among other things, log messages may be necessary to debug a +// cancellation-related problem.) +// +// Handle methods that produce output should observe the following rules: +// - If r.Time is the zero time, ignore the time. +// - If r.PC is zero, ignore it. +// - Attr's values should be resolved. +// - If an Attr's key and value are both the zero value, ignore the Attr. +// This can be tested with attr.Equal(Attr{}). +// - If a group's key is empty, inline the group's Attrs. +// - If a group has no Attrs (even if it has a non-empty key), +// ignore it. +func (h NRHandler) Handle(ctx context.Context, record slog.Record) error { + attrs := map[string]interface{}{} + + record.Attrs(func(attr slog.Attr) bool { + attrs[attr.Key] = attr.Value.Any() + return true + }) + + data := newrelic.LogData{ + Severity: record.Level.String(), + Timestamp: record.Time.UnixMilli(), + Message: record.Message, + Attributes: attrs, + } + if h.txn != nil { + h.txn.RecordLog(data) + } else { + h.app.RecordLog(data) + } + + return h.handler.Handle(ctx, record) +} + +// WithAttrs returns a new Handler whose attributes consist of +// both the receiver's attributes and the arguments. +// The Handler owns the slice: it may retain, modify or discard it. +func (h NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + handler := h.handler.WithAttrs(attrs) + return NRHandler{ + handler: handler, + app: h.app, + txn: h.txn, + } + +} + +// 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. +// +// How this qualification happens is up to the Handler, so long as +// this Handler's attribute keys differ from those of another Handler +// with a different sequence of group names. +// +// A Handler should treat WithGroup as starting a Group of Attrs that ends +// at the end of the log event. That is, +// +// logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2)) +// +// should behave like +// +// logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) +// +// If the name is empty, WithGroup returns the receiver. +func (h NRHandler) WithGroup(name string) slog.Handler { + handler := h.handler.WithGroup(name) + return NRHandler{ + handler: handler, + app: h.app, + txn: h.txn, + } +} + +// NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context. +// New Relic transaction value is taken from context. It cannot be set directly. +// This serves as a quality of life improvement for cases where slog.Default global instance is +// referenced, allowing to use slog methods directly and maintaining New Relic instrumentation. +type TransactionFromContextHandler struct { + NRHandler +} + +// WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic +// transaction from context. +func WithTransactionFromContext(handler NRHandler) TransactionFromContextHandler { + return TransactionFromContextHandler{handler} +} + +// Handle handles the Record. +// It will only be called when Enabled returns true. +// The Context argument is as for Enabled and NewRelic transaction. +// Canceling the context should not affect record processing. +// (Among other things, log messages may be necessary to debug a +// cancellation-related problem.) +// +// Handle methods that produce output should observe the following rules: +// - If r.Time is the zero time, ignore the time. +// - If r.PC is zero, ignore it. +// - Attr's values should be resolved. +// - If an Attr's key and value are both the zero value, ignore the Attr. +// This can be tested with attr.Equal(Attr{}). +// - If a group's key is empty, inline the group's Attrs. +// - If a group has no Attrs (even if it has a non-empty key), +// ignore it. +func (h TransactionFromContextHandler) Handle(ctx context.Context, record slog.Record) error { + if txn := newrelic.FromContext(ctx); txn != nil { + return h.NRHandler.WithTransaction(txn).Handle(ctx, record) + } + + return h.NRHandler.Handle(ctx, record) +} diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go new file mode 100644 index 000000000..4e3d6d774 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -0,0 +1,337 @@ +package nrslog + +import ( + "bytes" + "context" + "log/slog" + "os" + "strings" + "testing" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/internal/logcontext" + "github.com/newrelic/go-agent/v3/internal/sysinfo" + "github.com/newrelic/go-agent/v3/newrelic" +) + +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 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) + + log1 := string(out.String()) + + txn := app.StartTransaction("hi") + txnLog := WithTransaction(txn, log) + txnLog.Info(message) + txn.End() + + log2 := string(out.String()) + + attrString := `"string key"=val "int key"=1` + if !strings.Contains(log1, attrString) { + t.Errorf("expected %s to contain %s", log1, attrString) + } + + if !strings.Contains(log2, attrString) { + t.Errorf("expected %s to contain %s", log2, attrString) + } + +} + +func TestWithAttributesFromContext(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(false), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + log := slog.New(TextHandler(app.Application, os.Stdout, &slog.HandlerOptions{})) + + 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}), + ) + + txn.End() + + 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": 42, + "some_map": map[string]interface{}{"a": 1.0, "b": 2}, + }, + }, + }) + +} +func TestWithGroup(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.Group("test group", slog.String("string key", "val"))) + log = log.WithGroup("test group") + + log.Info(message) + + log1 := string(out.String()) + + txn := app.StartTransaction("hi") + txnLog := WithTransaction(txn, log) + txnLog.Info(message) + txn.End() + + log2 := string(out.String()) + + attrString := `"test group.string key"=val` + if !strings.Contains(log1, attrString) { + t.Errorf("expected %s to contain %s", log1, attrString) + } + + if !strings.Contains(log2, attrString) { + t.Errorf("expected %s to contain %s", log2, attrString) + } + +} + +func TestTransactionFromContextHandler(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, + }, + }) +} diff --git a/v3/integrations/logcontext-v2/nrslog/writer.go b/v3/integrations/logcontext-v2/nrslog/writer.go new file mode 100644 index 000000000..4248469a4 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/writer.go @@ -0,0 +1,70 @@ +package nrslog + +import ( + "bytes" + "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 NewWriter(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, + } +} + +// 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(p []byte) []byte { + logLine := bytes.TrimRight(p, "\n") + buf := bytes.NewBuffer(logLine) + + var enrichErr error + if b.txn != nil { + enrichErr = newrelic.EnrichLog(buf, newrelic.FromTxn(b.txn)) + } else { + 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(b.EnrichLog(p)) +} diff --git a/v3/integrations/logcontext-v2/nrwriter/go.mod b/v3/integrations/logcontext-v2/nrwriter/go.mod index 82df2d4e8..ed2a0e9c2 100644 --- a/v3/integrations/logcontext-v2/nrwriter/go.mod +++ b/v3/integrations/logcontext-v2/nrwriter/go.mod @@ -1,5 +1,8 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter -go 1.17 +go 1.20 -require github.com/newrelic/go-agent/v3 v3.19.1 +require github.com/newrelic/go-agent/v3 v3.33.1 + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrzap/LICENSE.txt b/v3/integrations/logcontext-v2/nrzap/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/logcontext-v2/nrzap/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/logcontext-v2/nrzap/example/main.go b/v3/integrations/logcontext-v2/nrzap/example/main.go new file mode 100644 index 000000000..84b1f0eda --- /dev/null +++ b/v3/integrations/logcontext-v2/nrzap/example/main.go @@ -0,0 +1,63 @@ +package 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) +} diff --git a/v3/integrations/logcontext-v2/nrzap/go.mod b/v3/integrations/logcontext-v2/nrzap/go.mod new file mode 100644 index 000000000..a813c150e --- /dev/null +++ b/v3/integrations/logcontext-v2/nrzap/go.mod @@ -0,0 +1,11 @@ +module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzap + +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + go.uber.org/zap v1.24.0 +) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap.go b/v3/integrations/logcontext-v2/nrzap/nrzap.go new file mode 100644 index 000000000..40bb34369 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrzap/nrzap.go @@ -0,0 +1,204 @@ +package 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. +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. +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) +} diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap_test.go b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go new file mode 100644 index 000000000..9ceeaa2aa --- /dev/null +++ b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go @@ -0,0 +1,375 @@ +package nrzap + +import ( + "errors" + "io" + "os" + "testing" + "time" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "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", + }, + 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": 1 * time.Second, + "int": 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": 1 * time.Second, + "int": 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") + } +} diff --git a/v3/integrations/logcontext-v2/nrzerolog/go.mod b/v3/integrations/logcontext-v2/nrzerolog/go.mod index f87763080..fb4593a92 100644 --- a/v3/integrations/logcontext-v2/nrzerolog/go.mod +++ b/v3/integrations/logcontext-v2/nrzerolog/go.mod @@ -1,8 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzerolog -go 1.17 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.18.0 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/rs/zerolog v1.26.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/zerologWriter/go.mod b/v3/integrations/logcontext-v2/zerologWriter/go.mod index afbbe5166..0087200af 100644 --- a/v3/integrations/logcontext-v2/zerologWriter/go.mod +++ b/v3/integrations/logcontext-v2/zerologWriter/go.mod @@ -1,9 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter -go 1.17 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.19.1 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.0 github.com/rs/zerolog v1.27.0 ) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext/nrlogrusplugin/go.mod b/v3/integrations/logcontext/nrlogrusplugin/go.mod index a503bb37c..15529e817 100644 --- a/v3/integrations/logcontext/nrlogrusplugin/go.mod +++ b/v3/integrations/logcontext/nrlogrusplugin/go.mod @@ -2,11 +2,13 @@ module 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.13 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 // v1.4.0 is required for for the log.WithContext. github.com/sirupsen/logrus v1.4.0 - golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect ) + + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/nramqp/examples/consumer/main.go b/v3/integrations/nramqp/examples/consumer/main.go new file mode 100644 index 000000000..5cfc92ec4 --- /dev/null +++ b/v3/integrations/nramqp/examples/consumer/main.go @@ -0,0 +1,78 @@ +package 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) + + conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") + 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) +} diff --git a/v3/integrations/nramqp/examples/publisher/main.go b/v3/integrations/nramqp/examples/publisher/main.go new file mode 100644 index 000000000..445947a08 --- /dev/null +++ b/v3/integrations/nramqp/examples/publisher/main.go @@ -0,0 +1,124 @@ +package 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 +} + +func NewServer(channel *amqp.Channel, exchangeName, routingKeyName string) *amqpServer { + return &amqpServer{ + channel, + exchangeName, + routingKeyName, + } +} + +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 + 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) + + conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") + 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) + + 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) +} diff --git a/v3/integrations/nramqp/go.mod b/v3/integrations/nramqp/go.mod new file mode 100644 index 000000000..3dcafdde2 --- /dev/null +++ b/v3/integrations/nramqp/go.mod @@ -0,0 +1,9 @@ +module github.com/newrelic/go-agent/v3/integrations/nramqp + +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/rabbitmq/amqp091-go v1.9.0 +) +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nramqp/headers.go b/v3/integrations/nramqp/headers.go new file mode 100644 index 000000000..d3604e493 --- /dev/null +++ b/v3/integrations/nramqp/headers.go @@ -0,0 +1,69 @@ +package 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 +} diff --git a/v3/integrations/nramqp/headers_test.go b/v3/integrations/nramqp/headers_test.go new file mode 100644 index 000000000..b5ee3548b --- /dev/null +++ b/v3/integrations/nramqp/headers_test.go @@ -0,0 +1,296 @@ +package nramqp + +import ( + "encoding/json" + "testing" + "time" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + + 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) + } +} diff --git a/v3/integrations/nramqp/nramqp.go b/v3/integrations/nramqp/nramqp.go new file mode 100644 index 000000000..2be8e7634 --- /dev/null +++ b/v3/integrations/nramqp/nramqp.go @@ -0,0 +1,106 @@ +package nramqp + +import ( + "context" + + amqp "github.com/rabbitmq/amqp091-go" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" +) + +const ( + RabbitMQLibrary = "RabbitMQ" +) + +func init() { internal.TrackUsage("integration", "messagebroker", "nramqp") } + +func creatProducerSegment(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 +} + +// 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 string, mandatory, immediate bool, msg amqp.Publishing) error { + txn := newrelic.FromContext(ctx) + if txn != nil { + // generate message broker segment + s := creatProducerSegment(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) + } + + integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageRoutingKey, key) + integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageCorrelationID, msg.CorrelationId) + integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeMessageReplyTo, msg.ReplyTo) + + // inject DT headers into headers object + msg.Headers = injectDtHeaders(txn, msg.Headers) + + s.StartTime = txn.StartSegmentNow() + 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.AttributeMessageQueueName, queue, 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 +} diff --git a/v3/integrations/nramqp/nramqp_test.go b/v3/integrations/nramqp/nramqp_test.go new file mode 100644 index 000000000..3db9e4ce9 --- /dev/null +++ b/v3/integrations/nramqp/nramqp_test.go @@ -0,0 +1,78 @@ +package 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++ { + creatProducerSegment("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 := creatProducerSegment(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) + } + } + +} diff --git a/v3/integrations/nrawsbedrock/LICENSE.txt b/v3/integrations/nrawsbedrock/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrawsbedrock/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrawsbedrock/README.md b/v3/integrations/nrawsbedrock/README.md new file mode 100644 index 000000000..a1038aea7 --- /dev/null +++ b/v3/integrations/nrawsbedrock/README.md @@ -0,0 +1,12 @@ +# 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). diff --git a/v3/integrations/nrawsbedrock/example/main.go b/v3/integrations/nrawsbedrock/example/main.go new file mode 100644 index 000000000..f767a42ee --- /dev/null +++ b/v3/integrations/nrawsbedrock/example/main.go @@ -0,0 +1,254 @@ +// +// 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() +} diff --git a/v3/integrations/nrawsbedrock/go.mod b/v3/integrations/nrawsbedrock/go.mod new file mode 100644 index 000000000..72edb5c9e --- /dev/null +++ b/v3/integrations/nrawsbedrock/go.mod @@ -0,0 +1,15 @@ +module github.com/newrelic/go-agent/v3/integrations/nrawsbedrock + +go 1.20 + +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.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 +) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrawsbedrock/nrawsbedrock.go b/v3/integrations/nrawsbedrock/nrawsbedrock.go new file mode 100644 index 000000000..640bf37d7 --- /dev/null +++ b/v3/integrations/nrawsbedrock/nrawsbedrock.go @@ -0,0 +1,1054 @@ +// 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/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" +) + +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 + + +***/ diff --git a/v3/integrations/nrawssdk-v1/go.mod b/v3/integrations/nrawssdk-v1/go.mod index ee1cc3936..b41c39600 100644 --- a/v3/integrations/nrawssdk-v1/go.mod +++ b/v3/integrations/nrawssdk-v1/go.mod @@ -3,10 +3,13 @@ module 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.7 +go 1.20 require ( // v1.15.0 is the first aws-sdk-go version with module support. - github.com/aws/aws-sdk-go v1.15.0 - github.com/newrelic/go-agent/v3 v3.16.0 + github.com/aws/aws-sdk-go v1.34.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrawssdk-v1/nrawssdk_test.go b/v3/integrations/nrawssdk-v1/nrawssdk_test.go index 79d20d9b0..ae1002bc5 100644 --- a/v3/integrations/nrawssdk-v1/nrawssdk_test.go +++ b/v3/integrations/nrawssdk-v1/nrawssdk_test.go @@ -25,7 +25,7 @@ import ( ) func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn) + return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } type fakeTransport struct{} diff --git a/v3/integrations/nrawssdk-v2/go.mod b/v3/integrations/nrawssdk-v2/go.mod index b5e1ae8dd..2a81df60a 100644 --- a/v3/integrations/nrawssdk-v2/go.mod +++ b/v3/integrations/nrawssdk-v2/go.mod @@ -2,7 +2,7 @@ module 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.17 +go 1.20 require ( github.com/aws/aws-sdk-go-v2 v1.16.15 @@ -11,5 +11,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/lambda v1.24.5 github.com/aws/aws-sdk-go-v2/service/s3 v1.27.10 github.com/aws/smithy-go v1.13.3 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrawssdk-v2/nrawssdk_test.go b/v3/integrations/nrawssdk-v2/nrawssdk_test.go index c57bba514..79b1f389a 100644 --- a/v3/integrations/nrawssdk-v2/nrawssdk_test.go +++ b/v3/integrations/nrawssdk-v2/nrawssdk_test.go @@ -23,7 +23,7 @@ import ( ) func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn) + return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } type fakeTransport struct{} diff --git a/v3/integrations/nrb3/go.mod b/v3/integrations/nrb3/go.mod index d3d2447d1..d69016b42 100644 --- a/v3/integrations/nrb3/go.mod +++ b/v3/integrations/nrb3/go.mod @@ -1 +1,8 @@ module github.com/newrelic/go-agent/v3/integrations/nrb3 + +go 1.20 + +require github.com/newrelic/go-agent/v3 v3.33.1 + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrecho-v3/go.mod b/v3/integrations/nrecho-v3/go.mod index ea754b5f4..a8b06b82b 100644 --- a/v3/integrations/nrecho-v3/go.mod +++ b/v3/integrations/nrecho-v3/go.mod @@ -2,12 +2,14 @@ module 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.7 +go 1.20 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/labstack/gommon v0.3.0 // indirect - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrecho-v3/nrecho.go b/v3/integrations/nrecho-v3/nrecho.go index ad5f862b8..05b8521a1 100644 --- a/v3/integrations/nrecho-v3/nrecho.go +++ b/v3/integrations/nrecho-v3/nrecho.go @@ -35,15 +35,31 @@ func handlerPointer(handler echo.HandlerFunc) uintptr { return reflect.ValueOf(handler).Pointer() } -func transactionName(c echo.Context) string { +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" + return "NotFoundHandler", "" } if ptr == handlerPointer(echo.MethodNotAllowedHandler) { - return "MethodNotAllowedHandler" + return "MethodNotAllowedHandler", "" } - return c.Request().Method + " " + c.Path() + return c.Request().Method + " " + c.Path(), c.Path() } // Middleware creates Echo middleware that instruments requests. @@ -51,9 +67,7 @@ func transactionName(c echo.Context) string { // 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 @@ -63,9 +77,12 @@ func Middleware(app *newrelic.Application) func(echo.HandlerFunc) echo.HandlerFu return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { rw := c.Response().Writer - txn := app.StartTransaction(transactionName(c)) + 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) @@ -93,3 +110,23 @@ func Middleware(app *newrelic.Application) func(echo.HandlerFunc) echo.HandlerFu } } } + +// 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)) + } + } +} diff --git a/v3/integrations/nrecho-v3/nrecho_test.go b/v3/integrations/nrecho-v3/nrecho_test.go index 177bda37d..21523398a 100644 --- a/v3/integrations/nrecho-v3/nrecho_test.go +++ b/v3/integrations/nrecho-v3/nrecho_test.go @@ -12,10 +12,11 @@ import ( "github.com/labstack/echo" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" + newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestBasicRoute(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -79,7 +80,7 @@ func TestNilApp(t *testing.T) { } func TestTransactionContext(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -109,7 +110,7 @@ func TestTransactionContext(t *testing.T) { } func TestNotFoundHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -129,7 +130,7 @@ func TestNotFoundHandler(t *testing.T) { } func TestMethodNotAllowedHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -154,7 +155,7 @@ func TestMethodNotAllowedHandler(t *testing.T) { } func TestReturnsHTTPError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -196,7 +197,7 @@ func TestReturnsHTTPError(t *testing.T) { } func TestReturnsError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -238,7 +239,7 @@ func TestReturnsError(t *testing.T) { } func TestResponseCode(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) diff --git a/v3/integrations/nrecho-v4/go.mod b/v3/integrations/nrecho-v4/go.mod index fd6e3a801..a4ca9897e 100644 --- a/v3/integrations/nrecho-v4/go.mod +++ b/v3/integrations/nrecho-v4/go.mod @@ -2,9 +2,12 @@ module 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.17 +go 1.20 require ( github.com/labstack/echo/v4 v4.9.0 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrecho-v4/nrecho.go b/v3/integrations/nrecho-v4/nrecho.go index 4c3b8cd18..130d04b29 100644 --- a/v3/integrations/nrecho-v4/nrecho.go +++ b/v3/integrations/nrecho-v4/nrecho.go @@ -35,15 +35,15 @@ func handlerPointer(handler echo.HandlerFunc) uintptr { return reflect.ValueOf(handler).Pointer() } -func transactionName(c echo.Context) string { +func transactionName(c echo.Context) (string, string) { ptr := handlerPointer(c.Handler()) if ptr == handlerPointer(echo.NotFoundHandler) { - return "NotFoundHandler" + return "NotFoundHandler", "" } if ptr == handlerPointer(echo.MethodNotAllowedHandler) { - return "MethodNotAllowedHandler" + return "MethodNotAllowedHandler", "" } - return c.Request().Method + " " + c.Path() + return c.Request().Method + " " + c.Path(), c.Path() } // Skipper defines a function to skip middleware. Returning true skips processing @@ -71,7 +71,6 @@ func WithSkipper(skipper Skipper) ConfigOption { // 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 { @@ -101,9 +100,12 @@ func Middleware(app *newrelic.Application, opts ...ConfigOption) func(echo.Handl } rw := c.Response().Writer - txn := config.App.StartTransaction(transactionName(c)) + 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) @@ -131,3 +133,23 @@ func Middleware(app *newrelic.Application, opts ...ConfigOption) func(echo.Handl } } } + +// 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) + } + } +} diff --git a/v3/integrations/nrecho-v4/nrecho_test.go b/v3/integrations/nrecho-v4/nrecho_test.go index 1a0debab2..70659d17a 100644 --- a/v3/integrations/nrecho-v4/nrecho_test.go +++ b/v3/integrations/nrecho-v4/nrecho_test.go @@ -12,10 +12,11 @@ import ( "github.com/labstack/echo/v4" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" + newrelic "github.com/newrelic/go-agent/v3/newrelic" ) func TestBasicRoute(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -61,7 +62,7 @@ func TestBasicRoute(t *testing.T) { } func TestSkipper(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() skipper := func(c echo.Context) bool { @@ -144,7 +145,7 @@ func TestNilApp(t *testing.T) { } func TestTransactionContext(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -174,7 +175,7 @@ func TestTransactionContext(t *testing.T) { } func TestNotFoundHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -194,7 +195,7 @@ func TestNotFoundHandler(t *testing.T) { } func TestMethodNotAllowedHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -219,7 +220,7 @@ func TestMethodNotAllowedHandler(t *testing.T) { } func TestReturnsHTTPError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -261,7 +262,7 @@ func TestReturnsHTTPError(t *testing.T) { } func TestReturnsError(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) @@ -303,7 +304,7 @@ func TestReturnsError(t *testing.T) { } func TestResponseCode(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) e := echo.New() e.Use(Middleware(app.Application)) diff --git a/v3/integrations/nrelasticsearch-v7/go.mod b/v3/integrations/nrelasticsearch-v7/go.mod index c01608e35..d8466fb6c 100644 --- a/v3/integrations/nrelasticsearch-v7/go.mod +++ b/v3/integrations/nrelasticsearch-v7/go.mod @@ -2,9 +2,12 @@ module 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.11 +go 1.20 require ( github.com/elastic/go-elasticsearch/v7 v7.17.0 - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod new file mode 100644 index 000000000..a0d677eff --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod @@ -0,0 +1,13 @@ +module client-example + +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + 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 => ../../../.. diff --git a/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go b/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go new file mode 100644 index 000000000..bb958b840 --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go @@ -0,0 +1,62 @@ +// 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) +} diff --git a/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod new file mode 100644 index 000000000..13278efc6 --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod @@ -0,0 +1,13 @@ +module server-example + +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + 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 => ../../../.. diff --git a/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go b/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go new file mode 100644 index 000000000..bdb642f85 --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go @@ -0,0 +1,59 @@ +// 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) +} diff --git a/v3/integrations/nrfasthttp/go.mod b/v3/integrations/nrfasthttp/go.mod new file mode 100644 index 000000000..f008cedb3 --- /dev/null +++ b/v3/integrations/nrfasthttp/go.mod @@ -0,0 +1,11 @@ +module github.com/newrelic/go-agent/v3/integrations/nrfasthttp + +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/valyala/fasthttp v1.49.0 +) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrfasthttp/instrumentation.go b/v3/integrations/nrfasthttp/instrumentation.go new file mode 100644 index 000000000..ff07a84d9 --- /dev/null +++ b/v3/integrations/nrfasthttp/instrumentation.go @@ -0,0 +1,83 @@ +package 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() { + newrelic.GetSecurityAgentInterface().SendEvent("INBOUND_WRITE", resp.Body(), resp.Header()) + newrelic.GetSecurityAgentInterface().SendEvent("INBOUND_RESPONSE_CODE", ctx.Response.StatusCode()) + } + } +} diff --git a/v3/integrations/nrfasthttp/instrumentation_test.go b/v3/integrations/nrfasthttp/instrumentation_test.go new file mode 100644 index 000000000..844c3682b --- /dev/null +++ b/v3/integrations/nrfasthttp/instrumentation_test.go @@ -0,0 +1,56 @@ +package 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}, + }) +} diff --git a/v3/integrations/nrfasthttp/segment.go b/v3/integrations/nrfasthttp/segment.go new file mode 100644 index 000000000..aa480f5f0 --- /dev/null +++ b/v3/integrations/nrfasthttp/segment.go @@ -0,0 +1,80 @@ +package 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 +} diff --git a/v3/integrations/nrfasthttp/segment_test.go b/v3/integrations/nrfasthttp/segment_test.go new file mode 100644 index 000000000..550e39a86 --- /dev/null +++ b/v3/integrations/nrfasthttp/segment_test.go @@ -0,0 +1,65 @@ +package nrfasthttp + +import ( + "testing" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "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}, + }) +} diff --git a/v3/integrations/nrgin/go.mod b/v3/integrations/nrgin/go.mod index 8ee5f5d72..b2d9ee716 100644 --- a/v3/integrations/nrgin/go.mod +++ b/v3/integrations/nrgin/go.mod @@ -2,9 +2,12 @@ module 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.12 +go 1.20 require ( - github.com/gin-gonic/gin v1.8.0 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/gin-gonic/gin v1.9.1 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgin/nrgin.go b/v3/integrations/nrgin/nrgin.go index 536b6b01e..03bb266c0 100644 --- a/v3/integrations/nrgin/nrgin.go +++ b/v3/integrations/nrgin/nrgin.go @@ -19,7 +19,7 @@ import ( "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" ) func init() { internal.TrackUsage("integration", "framework", "gin", "v1") } @@ -60,11 +60,17 @@ func (w *replacementResponseWriter) WriteHeader(code int) { 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) } @@ -138,6 +144,25 @@ 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) gin.HandlerFunc { return func(c *gin.Context) { if app != nil { @@ -145,6 +170,9 @@ func middleware(app *newrelic.Application, useNewNames bool) gin.HandlerFunc { 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() diff --git a/v3/integrations/nrgin/nrgin_test.go b/v3/integrations/nrgin/nrgin_test.go index 26cca960d..8249f592a 100644 --- a/v3/integrations/nrgin/nrgin_test.go +++ b/v3/integrations/nrgin/nrgin_test.go @@ -284,6 +284,10 @@ func TestStatusCodes(t *testing.T) { "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, }, }}) } @@ -338,6 +342,10 @@ func TestNoResponseBody(t *testing.T) { "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, }, }}) } diff --git a/v3/integrations/nrgorilla/go.mod b/v3/integrations/nrgorilla/go.mod index ac0c6c2e3..a8d1deef6 100644 --- a/v3/integrations/nrgorilla/go.mod +++ b/v3/integrations/nrgorilla/go.mod @@ -2,10 +2,13 @@ module 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.12 +go 1.20 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.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgorilla/nrgorilla.go b/v3/integrations/nrgorilla/nrgorilla.go index dd2088a25..81a0e2184 100644 --- a/v3/integrations/nrgorilla/nrgorilla.go +++ b/v3/integrations/nrgorilla/nrgorilla.go @@ -64,6 +64,18 @@ func routeName(r *http.Request) string { 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. // @@ -104,6 +116,9 @@ func Middleware(app *newrelic.Application) mux.MiddlewareFunc { 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) @@ -111,3 +126,34 @@ func Middleware(app *newrelic.Application) mux.MiddlewareFunc { }) } } + +// 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 + }) + } +} diff --git a/v3/integrations/nrgraphgophers/go.mod b/v3/integrations/nrgraphgophers/go.mod index e9dc8c31d..d2e57b713 100644 --- a/v3/integrations/nrgraphgophers/go.mod +++ b/v3/integrations/nrgraphgophers/go.mod @@ -2,10 +2,13 @@ module 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.13 +go 1.20 require ( // graphql-go has no tagged releases as of Jan 2020. - github.com/graph-gophers/graphql-go v0.0.0-20200207002730-8334863f2c8b - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/graph-gophers/graphql-go v1.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgraphqlgo/example/go.mod b/v3/integrations/nrgraphqlgo/example/go.mod index a0398cf07..bee83ed6a 100644 --- a/v3/integrations/nrgraphqlgo/example/go.mod +++ b/v3/integrations/nrgraphqlgo/example/go.mod @@ -1,14 +1,14 @@ module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo/example -go 1.13 +go 1.20 require ( - github.com/graphql-go/graphql v0.7.9 + 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.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo v1.0.0 ) -replace github.com/newrelic/go-agent/v3 => ../../../ - replace github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo => ../ + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/nrgraphqlgo/go.mod b/v3/integrations/nrgraphqlgo/go.mod index bb8eb0a3f..9f3a90a80 100644 --- a/v3/integrations/nrgraphqlgo/go.mod +++ b/v3/integrations/nrgraphqlgo/go.mod @@ -1,8 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo -go 1.13 +go 1.20 require ( - github.com/graphql-go/graphql v0.7.9 - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/graphql-go/graphql v0.8.1 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgrpc/go.mod b/v3/integrations/nrgrpc/go.mod index 07a866d15..2d76c537a 100644 --- a/v3/integrations/nrgrpc/go.mod +++ b/v3/integrations/nrgrpc/go.mod @@ -1,22 +1,19 @@ module github.com/newrelic/go-agent/v3/integrations/nrgrpc -// As of Dec 2019, the grpc go.mod file uses 1.11: -// https://github.com/grpc/grpc-go/blob/master/go.mod -go 1.17 +go 1.20 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.2 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/golang/protobuf v1.5.3 + github.com/newrelic/go-agent/v3 v3.33.1 + 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.49.0 + google.golang.org/grpc v1.56.3 + google.golang.org/protobuf v1.33.0 ) -require ( - golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect - golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect - golang.org/x/text v0.3.3 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - google.golang.org/protobuf v1.27.1 // indirect -) + +replace github.com/newrelic/go-agent/v3/integrations/nrsecurityagent => ../../integrations/nrsecurityagent + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgrpc/nrgrpc_client.go b/v3/integrations/nrgrpc/nrgrpc_client.go index 7965a1089..7d125c3fb 100644 --- a/v3/integrations/nrgrpc/nrgrpc_client.go +++ b/v3/integrations/nrgrpc/nrgrpc_client.go @@ -10,7 +10,7 @@ import ( "net/url" "strings" - newrelic "github.com/newrelic/go-agent/v3/newrelic" + "github.com/newrelic/go-agent/v3/newrelic" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -32,12 +32,24 @@ func getURL(method, target string) *url.URL { } } +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 - if txn := newrelic.FromContext(ctx); nil != txn { - seg = newrelic.StartExternalSegment(txn, nil) + 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 @@ -56,6 +68,13 @@ func startClientSegment(ctx context.Context, method, target string) (*newrelic.E 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) } } diff --git a/v3/integrations/nrgrpc/nrgrpc_client_test.go b/v3/integrations/nrgrpc/nrgrpc_client_test.go index e81356dd5..e237296c9 100644 --- a/v3/integrations/nrgrpc/nrgrpc_client_test.go +++ b/v3/integrations/nrgrpc/nrgrpc_client_test.go @@ -11,6 +11,7 @@ import ( "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" "github.com/newrelic/go-agent/v3/internal/integrationsupport" newrelic "github.com/newrelic/go-agent/v3/newrelic" @@ -75,7 +76,7 @@ func TestGetURL(t *testing.T) { } func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces) + return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { @@ -609,3 +610,15 @@ func TestClientStreamingError(t *testing.T) { }, }}) } + +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) + } +} diff --git a/v3/integrations/nrgrpc/nrgrpc_server.go b/v3/integrations/nrgrpc/nrgrpc_server.go index 6b28c84f0..fac1d0201 100644 --- a/v3/integrations/nrgrpc/nrgrpc_server.go +++ b/v3/integrations/nrgrpc/nrgrpc_server.go @@ -38,11 +38,14 @@ import ( "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 { @@ -60,37 +63,42 @@ func startTransaction(ctx context.Context, app *newrelic.Application, fullMethod 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: newrelic.TransportHTTP, + 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, @@ -111,13 +119,10 @@ var interceptorStatusHandlerRegistry = statusHandlerMap{ 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 @@ -125,73 +130,70 @@ type HandlerOption func(statusHandlerMap) // // 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 +// +// 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 +// +// 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) { -// ... -// } +// +// 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. // +// 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{ @@ -203,13 +205,11 @@ func ErrorInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transactio 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") @@ -217,13 +217,11 @@ func WarningInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transact 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") @@ -231,16 +229,12 @@ func InfoInterceptorStatusHandler(ctx context.Context, txn *newrelic.Transaction 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()] @@ -285,15 +279,16 @@ func reportInterceptorStatus(ctx context.Context, txn *newrelic.Transaction, han // 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))) +// +// 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 interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return handler(ctx, req) } } @@ -306,8 +301,13 @@ func UnaryServerInterceptor(app *newrelic.Application, options ...HandlerOption) option(localHandlerMap) } - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { + 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) @@ -327,6 +327,14 @@ func (s wrappedServerStream) Context() context.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, @@ -343,10 +351,9 @@ func newWrappedServerStream(stream grpc.ServerStream, txn *newrelic.Transaction) // 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 interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { return handler(srv, ss) } } @@ -359,12 +366,53 @@ func StreamServerInterceptor(app *newrelic.Application, options ...HandlerOption option(localHandlerMap) } - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + 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 +} diff --git a/v3/integrations/nrgrpc/nrgrpc_server_test.go b/v3/integrations/nrgrpc/nrgrpc_server_test.go index cfb961cc7..ea14f1343 100644 --- a/v3/integrations/nrgrpc/nrgrpc_server_test.go +++ b/v3/integrations/nrgrpc/nrgrpc_server_test.go @@ -12,6 +12,7 @@ import ( "testing" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" @@ -53,6 +54,207 @@ func newTestServerAndConn(t *testing.T, app *newrelic.Application) (*grpc.Server 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() @@ -143,6 +345,7 @@ func TestUnaryServerInterceptor(t *testing.T) { }, }, }) + } func TestUnaryServerInterceptorError(t *testing.T) { diff --git a/v3/integrations/nrhttprouter/go.mod b/v3/integrations/nrhttprouter/go.mod index 6d841a5e8..8a3ccdb75 100644 --- a/v3/integrations/nrhttprouter/go.mod +++ b/v3/integrations/nrhttprouter/go.mod @@ -2,10 +2,13 @@ module 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.7 +go 1.20 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.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrhttprouter/nrhttprouter.go b/v3/integrations/nrhttprouter/nrhttprouter.go index 75cb73ff4..8239dae1a 100644 --- a/v3/integrations/nrhttprouter/nrhttprouter.go +++ b/v3/integrations/nrhttprouter/nrhttprouter.go @@ -8,33 +8,33 @@ // httprouter.Router. Use an *nrhttprouter.Router in place of your // *httprouter.Router. Example: // -// package main +// package main // -// import ( -// "fmt" -// "net/http" -// "os" +// 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" -// ) +// "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) +// 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) +// // 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) -// } +// 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 @@ -74,6 +74,9 @@ func (r *Router) handle(method string, path string, original httprouter.Handle) 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() @@ -84,6 +87,9 @@ func (r *Router) handle(method string, path string, original httprouter.Handle) } } 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. diff --git a/v3/integrations/nrhttprouter/nrhttprouter_test.go b/v3/integrations/nrhttprouter/nrhttprouter_test.go index a267fed72..e9138f3d5 100644 --- a/v3/integrations/nrhttprouter/nrhttprouter_test.go +++ b/v3/integrations/nrhttprouter/nrhttprouter_test.go @@ -31,7 +31,7 @@ func TestMethodFunctions(t *testing.T) { } for _, md := range methodFuncs { - app := integrationsupport.NewBasicTestApp() + 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. @@ -75,7 +75,7 @@ func TestGetNoApplication(t *testing.T) { } func TestHandle(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + 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) { @@ -126,7 +126,7 @@ func TestHandle(t *testing.T) { } func TestHandler(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.Handler("GET", "/hello/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -195,7 +195,7 @@ func TestHandlerMissingApplication(t *testing.T) { } func TestHandlerFunc(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.HandlerFunc("GET", "/hello/", func(w http.ResponseWriter, r *http.Request) { @@ -222,7 +222,7 @@ func TestHandlerFunc(t *testing.T) { } func TestNotFound(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -292,7 +292,7 @@ func TestNotFoundMissingApplication(t *testing.T) { } func TestNotFoundNotSet(t *testing.T) { - app := integrationsupport.NewBasicTestApp() + app := integrationsupport.NewTestApp(nil, newrelic.ConfigCodeLevelMetricsEnabled(false)) router := New(app.Application) response := httptest.NewRecorder() diff --git a/v3/integrations/nrlambda/go.mod b/v3/integrations/nrlambda/go.mod index 90d175e14..9f12b1fac 100644 --- a/v3/integrations/nrlambda/go.mod +++ b/v3/integrations/nrlambda/go.mod @@ -1,10 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrlambda -// As of Dec 2019, the aws-lambda-go go.mod uses 1.12: -// https://github.com/aws/aws-lambda-go/blob/master/go.mod -go 1.12 +go 1.20 require ( - github.com/aws/aws-lambda-go v1.20.0 - github.com/newrelic/go-agent/v3 v3.4.0 + github.com/aws/aws-lambda-go v1.41.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrlambda/handler_test.go b/v3/integrations/nrlambda/handler_test.go index 1b7afff12..ae8e2922c 100644 --- a/v3/integrations/nrlambda/handler_test.go +++ b/v3/integrations/nrlambda/handler_test.go @@ -26,7 +26,7 @@ func testApp(getenv func(string) string, t *testing.T) *newrelic.Application { } cfg := newConfigInternal(getenv) - app, err := newrelic.NewApplication(cfg) + app, err := newrelic.NewApplication(cfg, newrelic.ConfigCodeLevelMetricsEnabled(false)) if nil != err { t.Fatal(err) } diff --git a/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go b/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go new file mode 100644 index 000000000..7ce3a5782 --- /dev/null +++ b/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go @@ -0,0 +1,97 @@ +// 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() +} diff --git a/v3/integrations/nrlogrus/example/main.go b/v3/integrations/nrlogrus/examples/server/main.go similarity index 100% rename from v3/integrations/nrlogrus/example/main.go rename to v3/integrations/nrlogrus/examples/server/main.go diff --git a/v3/integrations/nrlogrus/go.mod b/v3/integrations/nrlogrus/go.mod index 23792cd91..6ecb3fd54 100644 --- a/v3/integrations/nrlogrus/go.mod +++ b/v3/integrations/nrlogrus/go.mod @@ -2,11 +2,15 @@ module 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.13 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.0.0 + github.com/newrelic/go-agent/v3 v3.33.1 + 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.1.0 + github.com/sirupsen/logrus v1.8.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrlogrus/nrlogrus_test.go b/v3/integrations/nrlogrus/nrlogrus_test.go index e9fe3ac33..5ca3c2f24 100644 --- a/v3/integrations/nrlogrus/nrlogrus_test.go +++ b/v3/integrations/nrlogrus/nrlogrus_test.go @@ -5,10 +5,12 @@ package nrlogrus import ( "bytes" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/sirupsen/logrus" "strings" "testing" - "github.com/sirupsen/logrus" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" ) func bufferToStringAndReset(buf *bytes.Buffer) string { @@ -17,33 +19,101 @@ func bufferToStringAndReset(buf *bytes.Buffer) string { return s } -func TestLogrus(t *testing.T) { +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) } - // Now switch the level and test that debug is no longer enabled. - l.SetLevel(logrus.InfoLevel) - lg.Debug("lion", map[string]interface{}{"color": "yellow"}) - s = bufferToStringAndReset(buf) - if strings.Contains(s, "lion") || strings.Contains(s, "yellow") { + +} +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 enabled := lg.DebugEnabled(); enabled { - t.Error(enabled) + + 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) } - lg.Info("tiger", map[string]interface{}{"color": "orange"}) - s = bufferToStringAndReset(buf) 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) + } + +} diff --git a/v3/integrations/nrlogxi/go.mod b/v3/integrations/nrlogxi/go.mod index 0a3c3cb2b..eab91404f 100644 --- a/v3/integrations/nrlogxi/go.mod +++ b/v3/integrations/nrlogxi/go.mod @@ -2,13 +2,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrlogxi // As of Dec 2019, logxi requires 1.3+: // https://github.com/mgutz/logxi#requirements -go 1.7 +go 1.20 require ( - github.com/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.10 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect // '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.0.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrlogxi/nrlogxi_test.go b/v3/integrations/nrlogxi/nrlogxi_test.go new file mode 100644 index 000000000..6f4b9e513 --- /dev/null +++ b/v3/integrations/nrlogxi/nrlogxi_test.go @@ -0,0 +1,108 @@ +// 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/internal/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) + } +} diff --git a/v3/integrations/nrmicro/go.mod b/v3/integrations/nrmicro/go.mod index 032847d79..482f339a9 100644 --- a/v3/integrations/nrmicro/go.mod +++ b/v3/integrations/nrmicro/go.mod @@ -2,10 +2,16 @@ module 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.13 +go 1.20 + +toolchain go1.22.3 require ( - github.com/golang/protobuf v1.3.2 + github.com/golang/protobuf v1.5.4 github.com/micro/go-micro v1.8.0 - github.com/newrelic/go-agent/v3 v3.4.0 + github.com/newrelic/go-agent/v3 v3.33.1 + google.golang.org/protobuf v1.34.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrmicro/nrmicro.go b/v3/integrations/nrmicro/nrmicro.go index 5f73b51f9..804e531ff 100644 --- a/v3/integrations/nrmicro/nrmicro.go +++ b/v3/integrations/nrmicro/nrmicro.go @@ -5,6 +5,7 @@ package nrmicro import ( "context" + "io" "net/http" "net/url" "strings" @@ -15,9 +16,11 @@ import ( "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/internal/integrationsupport" "github.com/newrelic/go-agent/v3/newrelic" + protoV2 "google.golang.org/protobuf/proto" ) type nrWrapper struct { @@ -162,7 +165,19 @@ func HandlerWrapper(app *newrelic.Application) server.HandlerWrapper { return func(ctx context.Context, req server.Request, rsp interface{}) error { txn := startWebTransaction(ctx, app, req) defer txn.End() - err := fn(newrelic.NewContext(ctx, txn), req, rsp) + 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 { @@ -239,14 +254,58 @@ func startWebTransaction(ctx context.Context, app *newrelic.Application, req ser 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 +} diff --git a/v3/integrations/nrmicro/nrmicro_test.go b/v3/integrations/nrmicro/nrmicro_test.go index af4485c3a..6e5b08234 100644 --- a/v3/integrations/nrmicro/nrmicro_test.go +++ b/v3/integrations/nrmicro/nrmicro_test.go @@ -87,7 +87,7 @@ func getDTRequestHeaderVal(ctx context.Context) string { } func createTestApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(replyFn, cfgFn, integrationsupport.ConfigFullTraces) + return integrationsupport.NewTestApp(replyFn, cfgFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { diff --git a/v3/integrations/nrmongo/go.mod b/v3/integrations/nrmongo/go.mod index 8a7c7d4fb..10132f41f 100644 --- a/v3/integrations/nrmongo/go.mod +++ b/v3/integrations/nrmongo/go.mod @@ -2,10 +2,13 @@ module 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.17 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 // mongo-driver does not support modules as of Nov 2019. go.mongodb.org/mongo-driver v1.10.2 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrmongo/nrmongo.go b/v3/integrations/nrmongo/nrmongo.go index 0c7c9496c..c509962c1 100644 --- a/v3/integrations/nrmongo/nrmongo.go +++ b/v3/integrations/nrmongo/nrmongo.go @@ -33,10 +33,12 @@ package nrmongo import ( "context" "regexp" + "strings" "sync" "github.com/newrelic/go-agent/v3/internal" - newrelic "github.com/newrelic/go-agent/v3/newrelic" + "github.com/newrelic/go-agent/v3/newrelic" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/event" ) @@ -88,6 +90,8 @@ func NewCommandMonitor(original *event.CommandMonitor) *event.CommandMonitor { } 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) } @@ -95,6 +99,17 @@ func (m *mongoMonitor) started(ctx context.Context, e *event.CommandStartedEvent 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(), @@ -105,6 +120,9 @@ func (m *mongoMonitor) started(ctx context.Context, e *event.CommandStartedEvent PortPathOrID: port, DatabaseName: e.DatabaseName, } + if newrelic.IsSecurityAgentPresent() { + sgmt.SetSecureAgentEvent(secureAgentEvent) + } m.addSgmt(e, &sgmt) } @@ -121,6 +139,10 @@ func (m *mongoMonitor) addSgmt(e *event.CommandStartedEvent, sgmt *newrelic.Data } 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) @@ -147,6 +169,12 @@ func (m *mongoMonitor) getAndRemoveSgmt(id int64) *newrelic.DatastoreSegment { } 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) @@ -157,3 +185,12 @@ func calcHostAndPort(connID string) (host string, port string) { } return } + +func getJsonQuery(q interface{}) []byte { + map_json, err := bson.MarshalExtJSON(q, true, true) + if err != nil { + return []byte("") + } else { + return map_json + } +} diff --git a/v3/integrations/nrmongo/nrmongo_test.go b/v3/integrations/nrmongo/nrmongo_test.go index 6bc758c52..b6297fb8e 100644 --- a/v3/integrations/nrmongo/nrmongo_test.go +++ b/v3/integrations/nrmongo/nrmongo_test.go @@ -235,7 +235,7 @@ func TestCollName(t *testing.T) { } func createTestApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces) + return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var replyFn = func(reply *internal.ConnectReply) { diff --git a/v3/integrations/nrmssql/README.md b/v3/integrations/nrmssql/README.md index 6b58c772d..f543d86b9 100644 --- a/v3/integrations/nrmssql/README.md +++ b/v3/integrations/nrmssql/README.md @@ -1,6 +1,6 @@ # 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/denisenkom/go-mssqldb. +Package `nrmssql` instruments github.com/microsoft/go-mssqldb. ```go import "github.com/newrelic/go-agent/v3/integrations/nrmssql" diff --git a/v3/integrations/nrmssql/go.mod b/v3/integrations/nrmssql/go.mod index 1691dc2cf..f8df4469c 100644 --- a/v3/integrations/nrmssql/go.mod +++ b/v3/integrations/nrmssql/go.mod @@ -1,21 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrmssql -go 1.17 +go 1.20 require ( - github.com/denisenkom/go-mssqldb v0.12.2 - github.com/newrelic/go-agent/v3 v3.16.1 + github.com/microsoft/go-mssqldb v0.19.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) -require ( - github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect - github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/protobuf v1.4.3 // indirect - golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect - golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect - golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect - golang.org/x/text v0.3.6 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - google.golang.org/grpc v1.39.0 // indirect - google.golang.org/protobuf v1.25.0 // indirect -) + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrmssql/nrmssql.go b/v3/integrations/nrmssql/nrmssql.go index 19be91698..a78887432 100644 --- a/v3/integrations/nrmssql/nrmssql.go +++ b/v3/integrations/nrmssql/nrmssql.go @@ -1,9 +1,10 @@ // Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +//go:build go1.10 // +build go1.10 -// Package nrmssql instruments github.com/denisenkom/go-mssqldb. +// 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: @@ -13,7 +14,7 @@ // If your code is using sql.Open like this: // // import ( -// _ "github.com/denisenkom/go-mssqldb" +// _ "github.com/microsoft/go-mssqldb" // ) // // func main() { @@ -47,10 +48,11 @@ package nrmssql import ( "database/sql" "fmt" - "github.com/denisenkom/go-mssqldb/msdsn" - "github.com/newrelic/go-agent/v3/internal" - "github.com/denisenkom/go-mssqldb" + 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" ) @@ -71,7 +73,7 @@ func init() { } func parseDSN(s *newrelic.DatastoreSegment, dsn string) { - cfg, _, err := msdsn.Parse(dsn) + cfg, err := msdsn.Parse(dsn) if nil != err { return } diff --git a/v3/integrations/nrmysql/go.mod b/v3/integrations/nrmysql/go.mod index 5954af5cc..7c41cc27e 100644 --- a/v3/integrations/nrmysql/go.mod +++ b/v3/integrations/nrmysql/go.mod @@ -1,11 +1,14 @@ module github.com/newrelic/go-agent/v3/integrations/nrmysql // 1.10 is the Go version in mysql's go.mod -go 1.17 +go 1.20 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.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrmysql/nrmysql.go b/v3/integrations/nrmysql/nrmysql.go index a955856aa..a446515fe 100644 --- a/v3/integrations/nrmysql/nrmysql.go +++ b/v3/integrations/nrmysql/nrmysql.go @@ -1,6 +1,7 @@ // 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. @@ -48,6 +49,7 @@ package nrmysql import ( + "crypto/tls" "database/sql" "net" @@ -67,6 +69,10 @@ var ( } ) +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") diff --git a/v3/integrations/nrnats/go.mod b/v3/integrations/nrnats/go.mod index c8d0d40d8..909a95903 100644 --- a/v3/integrations/nrnats/go.mod +++ b/v3/integrations/nrnats/go.mod @@ -1,10 +1,14 @@ module github.com/newrelic/go-agent/v3/integrations/nrnats -// As of Dec 2019, 1.11 is the earliest version of Go tested by nats: +// 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.17 +go 1.20 require ( - github.com/nats-io/nats.go v1.17.0 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/nats-io/nats-server v1.4.1 + github.com/nats-io/nats.go v1.28.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrnats/test/nrnats_test.go b/v3/integrations/nrnats/nrnats_test.go similarity index 93% rename from v3/integrations/nrnats/test/nrnats_test.go rename to v3/integrations/nrnats/nrnats_test.go index e79d9dfc5..c044d2f52 100644 --- a/v3/integrations/nrnats/test/nrnats_test.go +++ b/v3/integrations/nrnats/nrnats_test.go @@ -11,7 +11,6 @@ import ( "github.com/nats-io/nats-server/test" nats "github.com/nats-io/nats.go" - "github.com/newrelic/go-agent/v3/integrations/nrnats" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" newrelic "github.com/newrelic/go-agent/v3/newrelic" @@ -24,7 +23,7 @@ func TestMain(m *testing.M) { } func testApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn) + return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var cfgFn = func(cfg *newrelic.Config) { @@ -45,7 +44,7 @@ func TestStartPublishSegmentNilTxn(t *testing.T) { } defer nc.Close() - nrnats.StartPublishSegment(nil, nc, "mysubject").End() + StartPublishSegment(nil, nc, "mysubject").End() } func TestStartPublishSegmentNilConn(t *testing.T) { @@ -53,7 +52,7 @@ func TestStartPublishSegmentNilConn(t *testing.T) { // metrics app := testApp() txn := app.StartTransaction("testing") - nrnats.StartPublishSegment(txn, nil, "mysubject").End() + StartPublishSegment(txn, nil, "mysubject").End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ @@ -75,7 +74,7 @@ func TestStartPublishSegmentBasic(t *testing.T) { } defer nc.Close() - nrnats.StartPublishSegment(txn, nc, "mysubject").End() + StartPublishSegment(txn, nc, "mysubject").End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ @@ -135,7 +134,7 @@ func TestSubWrapperWithNilApp(t *testing.T) { t.Fatal("Error connecting to NATS server", err) } wg := sync.WaitGroup{} - nc.Subscribe("subject1", nrnats.SubWrapper(nil, func(msg *nats.Msg) { + nc.Subscribe("subject1", SubWrapper(nil, func(msg *nats.Msg) { wg.Done() })) wg.Add(1) @@ -150,7 +149,7 @@ func TestSubWrapper(t *testing.T) { } wg := sync.WaitGroup{} app := testApp() - nc.QueueSubscribe("subject2", "queue1", WgWrapper(&wg, nrnats.SubWrapper(app.Application, func(msg *nats.Msg) {}))) + 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() @@ -201,7 +200,7 @@ func TestStartPublishSegmentNaming(t *testing.T) { for _, tc := range testCases { app := testApp() txn := app.StartTransaction("testing") - nrnats.StartPublishSegment(txn, nc, tc.subject).End() + StartPublishSegment(txn, nc, tc.subject).End() txn.End() app.ExpectMetrics(t, []internal.WantMetric{ diff --git a/v3/integrations/nrnats/test/go.mod b/v3/integrations/nrnats/test/go.mod index b8e73a03d..3f6ec494f 100644 --- a/v3/integrations/nrnats/test/go.mod +++ b/v3/integrations/nrnats/test/go.mod @@ -1,32 +1,15 @@ module github.com/newrelic/go-agent/v3/integrations/test // This module exists to avoid having extra nrnats module dependencies. - -go 1.17 +go 1.20 replace github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 => ../ -replace github.com/newrelic/go-agent/v3 v3.18.2 => ../../../ - require ( github.com/nats-io/nats-server v1.4.1 github.com/nats-io/nats.go v1.17.0 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 ) -require ( - github.com/golang/protobuf v1.5.2 // indirect - github.com/nats-io/gnatsd v1.4.1 // indirect - github.com/nats-io/go-nats v1.7.2 // indirect - github.com/nats-io/nats-server/v2 v2.9.0 // indirect - github.com/nats-io/nkeys v0.3.0 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 // indirect - golang.org/x/text v0.3.6 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - google.golang.org/grpc v1.49.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect -) +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/nropenai/LICENSE.txt b/v3/integrations/nropenai/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nropenai/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nropenai/examples/chatcompletion/chatcompletion_example.go b/v3/integrations/nropenai/examples/chatcompletion/chatcompletion_example.go new file mode 100644 index 000000000..e87b2f9bf --- /dev/null +++ b/v3/integrations/nropenai/examples/chatcompletion/chatcompletion_example.go @@ -0,0 +1,94 @@ +package 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) +} diff --git a/v3/integrations/nropenai/examples/chatcompletionfeedback/chatcompletionfeedback.go b/v3/integrations/nropenai/examples/chatcompletionfeedback/chatcompletionfeedback.go new file mode 100644 index 000000000..21caea010 --- /dev/null +++ b/v3/integrations/nropenai/examples/chatcompletionfeedback/chatcompletionfeedback.go @@ -0,0 +1,68 @@ +package 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) +} diff --git a/v3/integrations/nropenai/examples/chatcompletionstreaming/chatcompletionstreaming.go b/v3/integrations/nropenai/examples/chatcompletionstreaming/chatcompletionstreaming.go new file mode 100644 index 000000000..4b0eb7265 --- /dev/null +++ b/v3/integrations/nropenai/examples/chatcompletionstreaming/chatcompletionstreaming.go @@ -0,0 +1,126 @@ +package 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.Printf(response.Choices[0].Delta.Content) + } + stream.Close() + SendFeedback(app, *stream) + // Shutdown Application + app.Shutdown(5 * time.Second) +} diff --git a/v3/integrations/nropenai/examples/embeddings/embeddings_example.go b/v3/integrations/nropenai/examples/embeddings/embeddings_example.go new file mode 100644 index 000000000..421e4bd6a --- /dev/null +++ b/v3/integrations/nropenai/examples/embeddings/embeddings_example.go @@ -0,0 +1,84 @@ +package 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) +} diff --git a/v3/integrations/nropenai/go.mod b/v3/integrations/nropenai/go.mod new file mode 100644 index 000000000..5dd7d6899 --- /dev/null +++ b/v3/integrations/nropenai/go.mod @@ -0,0 +1,13 @@ +module github.com/newrelic/go-agent/v3/integrations/nropenai + +go 1.20 + +require ( + github.com/google/uuid v1.6.0 + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/pkoukk/tiktoken-go v0.1.6 + github.com/sashabaranov/go-openai v1.20.2 +) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nropenai/nropenai.go b/v3/integrations/nropenai/nropenai.go new file mode 100644 index 000000000..5febb32b6 --- /dev/null +++ b/v3/integrations/nropenai/nropenai.go @@ -0,0 +1,706 @@ +// 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/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "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 +} diff --git a/v3/integrations/nropenai/nropenai_test.go b/v3/integrations/nropenai/nropenai_test.go new file mode 100644 index 000000000..3fad3e7db --- /dev/null +++ b/v3/integrations/nropenai/nropenai_test.go @@ -0,0 +1,681 @@ +package nropenai + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "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", + }, + }}) + +} diff --git a/v3/integrations/nrpgx/README.md b/v3/integrations/nrpgx/README.md index 19944a488..fe5a924cf 100644 --- a/v3/integrations/nrpgx/README.md +++ b/v3/integrations/nrpgx/README.md @@ -1,4 +1,4 @@ -# v3/integrations/nrpq [![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) +# 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. diff --git a/v3/integrations/nrpgx/example/sqlx/go.mod b/v3/integrations/nrpgx/example/sqlx/go.mod index b40996eb1..23e247094 100644 --- a/v3/integrations/nrpgx/example/sqlx/go.mod +++ b/v3/integrations/nrpgx/example/sqlx/go.mod @@ -1,17 +1,11 @@ // 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.13 - +go 1.20 require ( - github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jmoiron/sqlx v1.2.0 - github.com/newrelic/go-agent/v3 v3.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/nrpgx v0.0.0 ) - -replace github.com/newrelic/go-agent/v3 => ../../../../ - replace github.com/newrelic/go-agent/v3/integrations/nrpgx => ../../ +replace github.com/newrelic/go-agent/v3 => ../../../.. diff --git a/v3/integrations/nrpgx/go.mod b/v3/integrations/nrpgx/go.mod index d8168f6f1..0a493f988 100644 --- a/v3/integrations/nrpgx/go.mod +++ b/v3/integrations/nrpgx/go.mod @@ -1,14 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrpgx -// As of Dec 2019, go 1.11 is the earliest version of Go tested by lib/pq: -// https://github.com/lib/pq/blob/master/.travis.yml -go 1.11 +go 1.20 require ( - github.com/jackc/pgx v3.6.2+incompatible // indirect - github.com/jackc/pgx/v4 v4.13.0 // indirect - github.com/newrelic/go-agent/v3 v3.3.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect - golang.org/x/text v0.3.7 // indirect + github.com/jackc/pgx v3.6.2+incompatible + github.com/jackc/pgx/v4 v4.18.2 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrpgx5/LICENSE.txt b/v3/integrations/nrpgx5/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrpgx5/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrpgx5/README.md b/v3/integrations/nrpgx5/README.md index 3e5070f66..011451499 100644 --- a/v3/integrations/nrpgx5/README.md +++ b/v3/integrations/nrpgx5/README.md @@ -1,6 +1,6 @@ # 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 `nrpgx` instruments https://github.com/jackc/pgx/v5. +Package `nrpgx5` instruments https://github.com/jackc/pgx/v5. ```go import "github.com/newrelic/go-agent/v3/integrations/nrpgx5" diff --git a/v3/integrations/nrpgx5/example/LICENSE.txt b/v3/integrations/nrpgx5/example/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrpgx5/example/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrpgx5/example/pgx/LICENSE.txt b/v3/integrations/nrpgx5/example/pgx/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrpgx5/example/pgx/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrpgx5/example/pgx/main.go b/v3/integrations/nrpgx5/example/pgx/main.go index 758af98bb..d156a6557 100644 --- a/v3/integrations/nrpgx5/example/pgx/main.go +++ b/v3/integrations/nrpgx5/example/pgx/main.go @@ -18,7 +18,7 @@ func main() { panic(err) } - cfg.Tracer = nrpgx5.NewTracer() + cfg.Tracer = nrpgx5.NewTracer(nrpgx5.WithQueryParameters(true)) conn, err := pgx.ConnectConfig(context.Background(), cfg) if err != nil { panic(err) @@ -46,6 +46,15 @@ func main() { 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) diff --git a/v3/integrations/nrpgx5/example/pgxpool/LICENSE.txt b/v3/integrations/nrpgx5/example/pgxpool/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrpgx5/example/pgxpool/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrpgx5/example/pgxpool/main.go b/v3/integrations/nrpgx5/example/pgxpool/main.go index 8457a478d..f7eb38329 100644 --- a/v3/integrations/nrpgx5/example/pgxpool/main.go +++ b/v3/integrations/nrpgx5/example/pgxpool/main.go @@ -7,19 +7,28 @@ import ( "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 main() { - cfg, err := pgxpool.ParseConfig("postgres://postgres:postgres@localhost:5432") +func NewPgxPool(ctx context.Context, dbURL string) (*pgxpool.Pool, error) { + cfg, err := pgxpool.ParseConfig(dbURL) if err != nil { - panic(err) + return nil, err + } + + cfg.BeforeConnect = func(_ context.Context, config *pgx.ConnConfig) error { + config.Tracer = nrpgx5.NewTracer() + return nil } - cfg.ConnConfig.Tracer = nrpgx5.NewTracer() - db, err := pgxpool.NewWithConfig(context.Background(), cfg) + return pgxpool.NewWithConfig(ctx, cfg) +} + +func main() { + db, err := NewPgxPool(context.Background(), "postgres://postgres:postgres@localhost:5432") if err != nil { panic(err) } diff --git a/v3/integrations/nrpgx5/go.mod b/v3/integrations/nrpgx5/go.mod index 318391be1..296c4a427 100644 --- a/v3/integrations/nrpgx5/go.mod +++ b/v3/integrations/nrpgx5/go.mod @@ -1,10 +1,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrpgx5 -go 1.17 +go 1.20 require ( github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 - github.com/jackc/pgx/v5 v5.0.3 - github.com/newrelic/go-agent/v3 v3.20.0 - github.com/stretchr/testify v1.8.0 + github.com/jackc/pgx/v5 v5.5.4 + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/stretchr/testify v1.8.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrpgx5/nrpgx5.go b/v3/integrations/nrpgx5/nrpgx5.go index da3f41265..6331064fb 100644 --- a/v3/integrations/nrpgx5/nrpgx5.go +++ b/v3/integrations/nrpgx5/nrpgx5.go @@ -1,55 +1,41 @@ +// 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 are the steps to instrument your pgx calls without using `database/sql`: -// if you want to use `database/sql`, you can use `nrpgx` package instead +// 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 pgx calls: -// you can set the tracer in the pgx.Config like this -// ```go -// import ( -// "github.com/jackc/pgx/v5" -// "github.com/newrelic/go-agent/v3/integrations/nrpgx5" -// "github.com/newrelic/go-agent/v3/newrelic" -// ) +// 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. // -// func main() { -// cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") -// if err != nil { -// panic(err) -// } +// For example: // -// cfg.Tracer = nrpgx5.NewTracer() -// conn, err := pgx.ConnectConfig(context.Background(), cfg) -// if err != nil { -// panic(err) -// } -// ... -// ``` -// or you can set the tracer in the pgxpool.Config like this -// ```go -// import ( -// "github.com/jackc/pgx/v5/pgxpool" -// "github.com/newrelic/go-agent/v3/integrations/nrpgx5" -// "github.com/newrelic/go-agent/v3/newrelic" -// ) +// 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 := pgxpool.ParseConfig("postgres://postgres:postgres@localhost:5432") -// if err != nil { -// panic(err) -// } +// func main() { +// cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") // OR pgxpools.ParseConfig(...) +// if err != nil { +// panic(err) +// } // -// cfg.ConnConfig.Tracer = nrpgx5.NewTracer() -// db, err := pgxpool.NewWithConfig(context.Background(), cfg) -// 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 ( @@ -68,8 +54,9 @@ func init() { type ( Tracer struct { - BaseSegment newrelic.DatastoreSegment - ParseQuery func(segment *newrelic.DatastoreSegment, query string) + BaseSegment newrelic.DatastoreSegment + ParseQuery func(segment *newrelic.DatastoreSegment, query string) + SendQueryParameters bool } nrPgxSegmentType string @@ -79,16 +66,58 @@ const ( querySegmentKey nrPgxSegmentType = "nrPgx5Segment" prepareSegmentKey nrPgxSegmentType = "prepareNrPgx5Segment" batchSegmentKey nrPgxSegmentType = "batchNrPgx5Segment" + querySecurityKey nrPgxSegmentType = "nrPgx5SecurityToken" ) -func NewTracer() *Tracer { - return &Tracer{ - ParseQuery: sqlparse.ParseQuery, +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 } -// TraceConnectStart is called at the beginning of Connect and ConnectConfig calls. The returned context is used for -// the rest of the call and will be passed to TraceConnectEnd. // implement pgx.ConnectTracer +// 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, @@ -100,29 +129,44 @@ func (t *Tracer) TraceConnectStart(ctx context.Context, data pgx.TraceConnectSta return ctx } -// TraceConnectEnd method // implement pgx.ConnectTracer +// 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 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. //implement pgx.QueryTracer +// 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 - segment.QueryParameters = t.getQueryParameters(data.Args) + 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 method implement pgx.QueryTracer. It will try to get segment from context and end it. +// 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) + ctx = context.WithValue(ctx, querySecurityKey, nil) + } + } segment.End() } @@ -135,7 +179,7 @@ func (t *Tracer) getQueryParameters(args []interface{}) map[string]interface{} { } // 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. // implement pgx.BatchTracer +// 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() @@ -145,7 +189,8 @@ func (t *Tracer) TraceBatchStart(ctx context.Context, conn *pgx.Conn, data pgx.T return context.WithValue(ctx, batchSegmentKey, &segment) } -// TraceBatchQuery implement pgx.BatchTracer. In this method we will get query and store it in 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 { @@ -155,7 +200,8 @@ func (t *Tracer) TraceBatchQuery(ctx context.Context, conn *pgx.Conn, data pgx.T segment.ParameterizedQuery += data.SQL + "\n" } -// TraceBatchEnd implement pgx.BatchTracer. In this method we will get segment from context and fill it with +// 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 { @@ -165,13 +211,14 @@ func (t *Tracer) TraceBatchEnd(ctx context.Context, conn *pgx.Conn, data pgx.Tra } // 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. // implement pgx.PrepareTracer -// The Query and QueryRow will call prepare. Fill this function will make the datastore segment called twice. -// So this function woudln't do anything and just return the context. +// 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 implement pgx.PrepareTracer. In this function nothing happens. +// TracePrepareEnd implements pgx.PrepareTracer. func (t *Tracer) TracePrepareEnd(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareEndData) { } diff --git a/v3/integrations/nrpkgerrors/go.mod b/v3/integrations/nrpkgerrors/go.mod index d7165e387..bbfa68b6f 100644 --- a/v3/integrations/nrpkgerrors/go.mod +++ b/v3/integrations/nrpkgerrors/go.mod @@ -2,11 +2,14 @@ module 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.11 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.0.0 + github.com/newrelic/go-agent/v3 v3.33.1 // 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 => ../.. diff --git a/v3/integrations/nrpkgerrors/nrkpgerrors_test.go b/v3/integrations/nrpkgerrors/nrkpgerrors_test.go index 17d1d62c0..3742bf33b 100644 --- a/v3/integrations/nrpkgerrors/nrkpgerrors_test.go +++ b/v3/integrations/nrpkgerrors/nrkpgerrors_test.go @@ -57,6 +57,7 @@ func TestWrappedStackTrace(t *testing.T) { {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 { @@ -117,6 +118,7 @@ func TestWrappedErrorClass(t *testing.T) { {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 { diff --git a/v3/integrations/nrpkgerrors/nrpkgerrors.go b/v3/integrations/nrpkgerrors/nrpkgerrors.go index 65af40e4b..e332ffac8 100644 --- a/v3/integrations/nrpkgerrors/nrpkgerrors.go +++ b/v3/integrations/nrpkgerrors/nrpkgerrors.go @@ -5,7 +5,6 @@ // // This package improves the class and stack-trace fields of pkg/error errors // when they are recorded with Transaction.NoticeError. -// package nrpkgerrors import ( @@ -76,10 +75,22 @@ func errorClass(e error) string { 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: diff --git a/v3/integrations/nrpq/example/main.go b/v3/integrations/nrpq/example/main.go index 3027db1de..55ce56cae 100644 --- a/v3/integrations/nrpq/example/main.go +++ b/v3/integrations/nrpq/example/main.go @@ -37,6 +37,7 @@ func main() { newrelic.ConfigAppName("PostgreSQL App"), newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), newrelic.ConfigDebugLogger(os.Stdout), + newrelic.ConfigDatastoreRawQuery(true), ) if nil != err { panic(err) diff --git a/v3/integrations/nrpq/example/sqlx/go.mod b/v3/integrations/nrpq/example/sqlx/go.mod index 3b441722f..8cd169014 100644 --- a/v3/integrations/nrpq/example/sqlx/go.mod +++ b/v3/integrations/nrpq/example/sqlx/go.mod @@ -1,17 +1,12 @@ // 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.13 - +go 1.20 require ( github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.1.0 - github.com/newrelic/go-agent/v3 v3.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 github.com/newrelic/go-agent/v3/integrations/nrpq v0.0.0 ) - -replace github.com/newrelic/go-agent/v3 => ../../../../ - replace github.com/newrelic/go-agent/v3/integrations/nrpq => ../../ +replace github.com/newrelic/go-agent/v3 => ../../../.. diff --git a/v3/integrations/nrpq/go.mod b/v3/integrations/nrpq/go.mod index 8dfe2fc43..ae0452c72 100644 --- a/v3/integrations/nrpq/go.mod +++ b/v3/integrations/nrpq/go.mod @@ -1,17 +1,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrpq -// As of Dec 2019, go 1.11 is the earliest version of Go tested by lib/pq: -// https://github.com/lib/pq/blob/master/.travis.yml -go 1.11 +go 1.20 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.3.0 - google.golang.org/grpc v1.27.0 // indirect + github.com/newrelic/go-agent/v3 v3.33.1 ) -replace github.com/newrelic/go-agent/v3 v3.3.0 => ../../../v3 -replace github.com/newrelic/go-agent/v3/newrelic => ../../../v3/newrelic +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrredis-v7/go.mod b/v3/integrations/nrredis-v7/go.mod index 16f15925f..6eb4e6f12 100644 --- a/v3/integrations/nrredis-v7/go.mod +++ b/v3/integrations/nrredis-v7/go.mod @@ -1,10 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrredis-v7 -// As of Jan 2020, go 1.11 is in the go-redis go.mod file: // https://github.com/go-redis/redis/blob/master/go.mod -go 1.11 +go 1.20 require ( github.com/go-redis/redis/v7 v7.0.0-beta.5 - github.com/newrelic/go-agent/v3 v3.17.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrredis-v8/go.mod b/v3/integrations/nrredis-v8/go.mod index cfa36aa15..7f184a876 100644 --- a/v3/integrations/nrredis-v8/go.mod +++ b/v3/integrations/nrredis-v8/go.mod @@ -1,10 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrredis-v8 -// As of Jan 2020, go 1.11 is in the go-redis go.mod file: // https://github.com/go-redis/redis/blob/master/go.mod -go 1.11 +go 1.20 require ( github.com/go-redis/redis/v8 v8.4.0 - github.com/newrelic/go-agent/v3 v3.0.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrredis-v8/nrredis_test.go b/v3/integrations/nrredis-v8/nrredis_test.go index 9cadb1aa3..58973dc67 100644 --- a/v3/integrations/nrredis-v8/nrredis_test.go +++ b/v3/integrations/nrredis-v8/nrredis_test.go @@ -44,6 +44,8 @@ func TestPing(t *testing.T) { {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}, }) } @@ -74,6 +76,8 @@ func TestPingWithOptionsAndAddress(t *testing.T) { {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}, }) } diff --git a/v3/integrations/nrredis-v9/LICENSE.txt b/v3/integrations/nrredis-v9/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrredis-v9/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrredis-v9/README.md b/v3/integrations/nrredis-v9/README.md new file mode 100644 index 000000000..b4214a917 --- /dev/null +++ b/v3/integrations/nrredis-v9/README.md @@ -0,0 +1,10 @@ +# 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). diff --git a/v3/integrations/nrredis-v9/example/main.go b/v3/integrations/nrredis-v9/example/main.go new file mode 100644 index 000000000..a0730ebcd --- /dev/null +++ b/v3/integrations/nrredis-v9/example/main.go @@ -0,0 +1,86 @@ +// 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) +} diff --git a/v3/integrations/nrredis-v9/go.mod b/v3/integrations/nrredis-v9/go.mod new file mode 100644 index 000000000..9134cee4b --- /dev/null +++ b/v3/integrations/nrredis-v9/go.mod @@ -0,0 +1,12 @@ +module github.com/newrelic/go-agent/v3/integrations/nrredis-v9 + +// https://github.com/redis/go-redis/blob/a38f75b640398bd709ee46c778a23e80e09d48b5/go.mod#L3 +go 1.20 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/redis/go-redis/v9 v9.0.2 +) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrredis-v9/nrredis.go b/v3/integrations/nrredis-v9/nrredis.go new file mode 100644 index 000000000..6f563acc6 --- /dev/null +++ b/v3/integrations/nrredis-v9/nrredis.go @@ -0,0 +1,109 @@ +// 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" + "net" + "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 { + segment newrelic.DatastoreSegment +} + +var _ redis.Hook = (*hook)(nil) + +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 { + 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) + 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 + } +} diff --git a/v3/integrations/nrredis-v9/nrredis_example_test.go b/v3/integrations/nrredis-v9/nrredis_example_test.go new file mode 100644 index 000000000..cba5c5138 --- /dev/null +++ b/v3/integrations/nrredis-v9/nrredis_example_test.go @@ -0,0 +1,54 @@ +// 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) +} diff --git a/v3/integrations/nrredis-v9/nrredis_test.go b/v3/integrations/nrredis-v9/nrredis_test.go new file mode 100644 index 000000000..4af825333 --- /dev/null +++ b/v3/integrations/nrredis-v9/nrredis_test.go @@ -0,0 +1,194 @@ +// 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" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + newrelic "github.com/newrelic/go-agent/v3/newrelic" + 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) + } + }) + } +} diff --git a/v3/integrations/nrsarama/LICENSE.txt b/v3/integrations/nrsarama/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrsarama/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrsarama/consumer.go b/v3/integrations/nrsarama/consumer.go new file mode 100644 index 000000000..a8aabf15c --- /dev/null +++ b/v3/integrations/nrsarama/consumer.go @@ -0,0 +1,129 @@ +package 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() + +} diff --git a/v3/integrations/nrsarama/example/consumer/consumerexample.go b/v3/integrations/nrsarama/example/consumer/consumerexample.go new file mode 100644 index 000000000..8f050626d --- /dev/null +++ b/v3/integrations/nrsarama/example/consumer/consumerexample.go @@ -0,0 +1,70 @@ +package 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() + +} diff --git a/v3/integrations/nrsarama/example/producer/producerexample.go b/v3/integrations/nrsarama/example/producer/producerexample.go new file mode 100644 index 000000000..f147c9b27 --- /dev/null +++ b/v3/integrations/nrsarama/example/producer/producerexample.go @@ -0,0 +1,66 @@ +package 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) +} diff --git a/v3/integrations/nrsarama/go.mod b/v3/integrations/nrsarama/go.mod new file mode 100644 index 000000000..1d2757a13 --- /dev/null +++ b/v3/integrations/nrsarama/go.mod @@ -0,0 +1,12 @@ +module github.com/newrelic/go-agent/v3/integrations/nrsarama + +go 1.20 + +require ( + github.com/Shopify/sarama v1.38.1 + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/stretchr/testify v1.8.1 +) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrsarama/nrsarama_test.go b/v3/integrations/nrsarama/nrsarama_test.go new file mode 100644 index 000000000..7a2223f80 --- /dev/null +++ b/v3/integrations/nrsarama/nrsarama_test.go @@ -0,0 +1,172 @@ +package 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/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "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"}, + }) + +} diff --git a/v3/integrations/nrsarama/producer.go b/v3/integrations/nrsarama/producer.go new file mode 100644 index 000000000..36c823f83 --- /dev/null +++ b/v3/integrations/nrsarama/producer.go @@ -0,0 +1,76 @@ +package 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 + +} diff --git a/v3/integrations/nrsecureagent/README.md b/v3/integrations/nrsecureagent/README.md new file mode 100644 index 000000000..6a03ab4c0 --- /dev/null +++ b/v3/integrations/nrsecureagent/README.md @@ -0,0 +1,4 @@ +# 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. diff --git a/v3/integrations/nrsecurityagent/LICENSE.txt b/v3/integrations/nrsecurityagent/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrsecurityagent/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrsecurityagent/README.md b/v3/integrations/nrsecurityagent/README.md new file mode 100644 index 000000000..7ad9fb5b0 --- /dev/null +++ b/v3/integrations/nrsecurityagent/README.md @@ -0,0 +1,80 @@ +# 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): +``` +enabled: true + + # NR security provides two modes IAST and RASP + # Default is IAST +mode: IAST + + # New Relic’s SaaS connection URLs +validator_service_url: wss://csec.nr-data.net + + # Following category of security events + # can be disabled from generating. +detection: + rxss: + enabled: true +request: + body_limit:1 +``` + +* 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). diff --git a/v3/integrations/nrsecurityagent/example/example.go b/v3/integrations/nrsecurityagent/example/example.go new file mode 100644 index 000000000..a722ec1c7 --- /dev/null +++ b/v3/integrations/nrsecurityagent/example/example.go @@ -0,0 +1,105 @@ +// 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) +} diff --git a/v3/integrations/nrsecurityagent/go.mod b/v3/integrations/nrsecurityagent/go.mod new file mode 100644 index 000000000..aae7f2977 --- /dev/null +++ b/v3/integrations/nrsecurityagent/go.mod @@ -0,0 +1,13 @@ +module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent + +go 1.20 + +require ( + github.com/newrelic/csec-go-agent v1.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 + 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 => ../.. diff --git a/v3/integrations/nrsecurityagent/nrsecurityagent.go b/v3/integrations/nrsecurityagent/nrsecurityagent.go new file mode 100644 index 000000000..224d1fe7f --- /dev/null +++ b/v3/integrations/nrsecurityagent/nrsecurityagent.go @@ -0,0 +1,184 @@ +// Copyright 2022 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package nrsecurityagent + +import ( + "fmt" + "io/ioutil" + "os" + "strconv" + + 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 + 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_DETECTION_RXSS_ENABLED (boolean) +// NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT (integer) set limit on read request body in kb. By default, this is "300" + +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") + } +} + +// 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 + } +} + +// 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 + } +} diff --git a/v3/integrations/nrslog/LICENSE.txt b/v3/integrations/nrslog/LICENSE.txt new file mode 100644 index 000000000..cee548c2d --- /dev/null +++ b/v3/integrations/nrslog/LICENSE.txt @@ -0,0 +1,206 @@ + 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. diff --git a/v3/integrations/nrslog/README.md b/v3/integrations/nrslog/README.md new file mode 100644 index 000000000..29673ad29 --- /dev/null +++ b/v3/integrations/nrslog/README.md @@ -0,0 +1,10 @@ +# 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). diff --git a/v3/integrations/nrslog/example_test.go b/v3/integrations/nrslog/example_test.go new file mode 100644 index 000000000..24b1ecb52 --- /dev/null +++ b/v3/integrations/nrslog/example_test.go @@ -0,0 +1,172 @@ +package 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()) + }) + } +} diff --git a/v3/integrations/nrslog/go.mod b/v3/integrations/nrslog/go.mod new file mode 100644 index 000000000..aad673e83 --- /dev/null +++ b/v3/integrations/nrslog/go.mod @@ -0,0 +1,11 @@ +module 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.21 + +require ( + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/stretchr/testify v1.9.0 +) + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrslog/nrslog.go b/v3/integrations/nrslog/nrslog.go new file mode 100644 index 000000000..9e8f079d1 --- /dev/null +++ b/v3/integrations/nrslog/nrslog.go @@ -0,0 +1,53 @@ +// 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)) +} diff --git a/v3/integrations/nrsnowflake/go.mod b/v3/integrations/nrsnowflake/go.mod index f7de99800..8694527a6 100644 --- a/v3/integrations/nrsnowflake/go.mod +++ b/v3/integrations/nrsnowflake/go.mod @@ -1,10 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrsnowflake -// snowflakedb/gosnowflake says it requires 1.12 but builds on 1.10 -go 1.10 +go 1.20 require ( - // v3.3.0 includes the new location of ParseQuery - github.com/newrelic/go-agent/v3 v3.3.0 - github.com/snowflakedb/gosnowflake v1.3.4 + github.com/newrelic/go-agent/v3 v3.33.1 + github.com/snowflakedb/gosnowflake v1.6.19 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrsnowflake/nrsnowflake.go b/v3/integrations/nrsnowflake/nrsnowflake.go index cfe6c570b..f630ee7b0 100644 --- a/v3/integrations/nrsnowflake/nrsnowflake.go +++ b/v3/integrations/nrsnowflake/nrsnowflake.go @@ -1,8 +1,6 @@ // Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// +build go1.10 - // Package nrsnowflake instruments github.com/snowflakedb/gosnowflake // // Use this package to instrument your Snowflake calls without having to manually diff --git a/v3/integrations/nrsqlite3/go.mod b/v3/integrations/nrsqlite3/go.mod index 3598eda33..def941a1f 100644 --- a/v3/integrations/nrsqlite3/go.mod +++ b/v3/integrations/nrsqlite3/go.mod @@ -2,10 +2,13 @@ module 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.9 +go 1.20 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.3.0 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrstan/examples/go.mod b/v3/integrations/nrstan/examples/go.mod index 90ab4311e..f3b544556 100644 --- a/v3/integrations/nrstan/examples/go.mod +++ b/v3/integrations/nrstan/examples/go.mod @@ -1,18 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan/examples - // This module exists to avoid a dependency on nrnrats. - -go 1.13 - +go 1.20 require ( github.com/nats-io/stan.go v0.5.0 - github.com/newrelic/go-agent/v3 v3.4.0 + github.com/newrelic/go-agent/v3 v3.33.1 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 => ../../../ - 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 => ../../.. diff --git a/v3/integrations/nrstan/go.mod b/v3/integrations/nrstan/go.mod index 865852338..fc7908b79 100644 --- a/v3/integrations/nrstan/go.mod +++ b/v3/integrations/nrstan/go.mod @@ -2,9 +2,14 @@ module 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.17 +go 1.20 + +toolchain go1.22.3 require ( - github.com/nats-io/stan.go v0.10.3 - github.com/newrelic/go-agent/v3 v3.18.2 + github.com/nats-io/stan.go v0.10.4 + github.com/newrelic/go-agent/v3 v3.33.1 ) + + +replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrstan/test/go.mod b/v3/integrations/nrstan/test/go.mod index aaeff8656..bb0001b15 100644 --- a/v3/integrations/nrstan/test/go.mod +++ b/v3/integrations/nrstan/test/go.mod @@ -2,16 +2,18 @@ module 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.20 -go 1.13 +toolchain go1.22.3 require ( - github.com/nats-io/nats-streaming-server v0.24.3 - github.com/nats-io/stan.go v0.10.3 - github.com/newrelic/go-agent/v3 v3.18.2 + 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.33.1 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) -replace github.com/newrelic/go-agent/v3 => ../../../ replace github.com/newrelic/go-agent/v3/integrations/nrstan => ../ + +replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/nrstan/test/nrstan_test.go b/v3/integrations/nrstan/test/nrstan_test.go index a41ae44d7..1f77c5f1b 100644 --- a/v3/integrations/nrstan/test/nrstan_test.go +++ b/v3/integrations/nrstan/test/nrstan_test.go @@ -31,7 +31,7 @@ func TestMain(m *testing.M) { } func createTestApp() integrationsupport.ExpectApp { - return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn) + return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.ConfigFullTraces, cfgFn, newrelic.ConfigCodeLevelMetricsEnabled(false)) } var cfgFn = func(cfg *newrelic.Config) { diff --git a/v3/integrations/nrzap/example_test.go b/v3/integrations/nrzap/example_test.go index d57343cd8..1bc161b1b 100644 --- a/v3/integrations/nrzap/example_test.go +++ b/v3/integrations/nrzap/example_test.go @@ -4,9 +4,14 @@ 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() { @@ -20,3 +25,79 @@ func Example() { 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") + } +} diff --git a/v3/integrations/nrzap/go.mod b/v3/integrations/nrzap/go.mod index 5c25d57e3..34ce78a3b 100644 --- a/v3/integrations/nrzap/go.mod +++ b/v3/integrations/nrzap/go.mod @@ -2,10 +2,13 @@ module 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.13 +go 1.20 require ( - github.com/newrelic/go-agent/v3 v3.0.0 + github.com/newrelic/go-agent/v3 v3.33.1 // 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 => ../.. diff --git a/v3/internal/cat/headers.go b/v3/internal/cat/headers.go index 6ca05cd67..c08fec28c 100644 --- a/v3/internal/cat/headers.go +++ b/v3/internal/cat/headers.go @@ -13,4 +13,5 @@ const ( NewRelicTxnName = "X-Newrelic-Transaction" NewRelicAppDataName = "X-Newrelic-App-Data" NewRelicSyntheticsName = "X-Newrelic-Synthetics" + NewRelicSyntheticsInfo = "X-Newrelic-Synthetics-Info" ) diff --git a/v3/internal/cat/synthetics.go b/v3/internal/cat/synthetics.go index b88c15476..59974f0a2 100644 --- a/v3/internal/cat/synthetics.go +++ b/v3/internal/cat/synthetics.go @@ -18,13 +18,30 @@ type SyntheticsHeader struct { 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") - 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") + 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 @@ -83,3 +100,80 @@ func (s *SyntheticsHeader) UnmarshalJSON(data []byte) error { 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 +} diff --git a/v3/internal/cat/synthetics_test.go b/v3/internal/cat/synthetics_test.go index 120d1f53c..b20b01f94 100644 --- a/v3/internal/cat/synthetics_test.go +++ b/v3/internal/cat/synthetics_test.go @@ -5,6 +5,7 @@ package cat import ( "encoding/json" + "fmt" "testing" ) @@ -118,3 +119,129 @@ func TestSyntheticsUnmarshalValid(t *testing.T) { } } } + +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) +} diff --git a/v3/internal/connect_reply.go b/v3/internal/connect_reply.go index 44772a3e4..b5ffaf9fe 100644 --- a/v3/internal/connect_reply.go +++ b/v3/internal/connect_reply.go @@ -100,6 +100,7 @@ type ConnectReply struct { 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"` diff --git a/v3/internal/crossagent/cross_agent_tests/docker_container_id/cases.json b/v3/internal/crossagent/cross_agent_tests/docker_container_id/cases.json index f1f9c0e61..4f9717792 100644 --- a/v3/internal/crossagent/cross_agent_tests/docker_container_id/cases.json +++ b/v3/internal/crossagent/cross_agent_tests/docker_container_id/cases.json @@ -1,4 +1,9 @@ [ + { + "filename": "mountinfo.txt", + "containerId": "ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5", + "expectedMetrics": null + }, { "filename": "docker-0.9.1.txt", "containerId": "f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee", diff --git a/v3/internal/crossagent/cross_agent_tests/docker_container_id/mountinfo.txt b/v3/internal/crossagent/cross_agent_tests/docker_container_id/mountinfo.txt new file mode 100644 index 000000000..cb1f3850a --- /dev/null +++ b/v3/internal/crossagent/cross_agent_tests/docker_container_id/mountinfo.txt @@ -0,0 +1,21 @@ +787 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 ro \ No newline at end of file diff --git a/v3/internal/expect.go b/v3/internal/expect.go index 2638225fd..6ed3d4f46 100644 --- a/v3/internal/expect.go +++ b/v3/internal/expect.go @@ -22,17 +22,19 @@ 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 { - Severity string - Message string - SpanID string - TraceID string - Timestamp int64 + Attributes map[string]interface{} + Severity string + Message string + SpanID string + TraceID string + Timestamp int64 } func uniquePointer() *struct{} { diff --git a/v3/internal/integrationsupport/integrationsupport_test.go b/v3/internal/integrationsupport/integrationsupport_test.go index 7e6912cee..1b2561c5f 100644 --- a/v3/internal/integrationsupport/integrationsupport_test.go +++ b/v3/internal/integrationsupport/integrationsupport_test.go @@ -31,6 +31,7 @@ func testApp(t *testing.T) *newrelic.Application { newrelic.ConfigLicense("0123456789012345678901234567890123456789"), newrelic.ConfigEnabled(false), newrelic.ConfigDistributedTracerEnabled(true), + newrelic.ConfigCodeLevelMetricsEnabled(false), ) if nil != err { t.Fatal(err) diff --git a/v3/internal/limits.go b/v3/internal/limits.go index b0f8228ec..2ed2f6125 100644 --- a/v3/internal/limits.go +++ b/v3/internal/limits.go @@ -25,4 +25,7 @@ const ( // 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 ) diff --git a/v3/internal/sysinfo/docker.go b/v3/internal/sysinfo/docker.go index 97deee896..d7499bb22 100644 --- a/v3/internal/sysinfo/docker.go +++ b/v3/internal/sysinfo/docker.go @@ -12,6 +12,7 @@ import ( "os" "regexp" "runtime" + "strings" ) var ( @@ -31,8 +32,18 @@ func DockerID() (string, error) { return "", err } defer f.Close() + id, err := parseDockerID(f) - return 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 ( @@ -43,6 +54,27 @@ var ( 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 @@ -51,7 +83,6 @@ func parseDockerID(r io.Reader) (string, error) { // // Example // 5:cpuacct,cpu,cpuset:/daemons - var id string for scanner := bufio.NewScanner(r); scanner.Scan(); { @@ -77,7 +108,6 @@ func parseDockerID(r io.Reader) (string, error) { } return id, nil } - return "", ErrDockerNotFound } diff --git a/v3/internal/sysinfo/docker_test.go b/v3/internal/sysinfo/docker_test.go index 0bfd20af3..485383e9a 100644 --- a/v3/internal/sysinfo/docker_test.go +++ b/v3/internal/sysinfo/docker_test.go @@ -33,7 +33,11 @@ func TestDockerIDCrossAgent(t *testing.T) { got, _ := parseDockerID(bytes.NewReader(input)) if got != test.ID { - t.Errorf("%s != %s", 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) + } } } } diff --git a/v3/internal/utilities.go b/v3/internal/utilities.go index e6c78e5ca..d57538b9a 100644 --- a/v3/internal/utilities.go +++ b/v3/internal/utilities.go @@ -7,6 +7,8 @@ import ( "bytes" "encoding/json" "fmt" + "reflect" + "runtime" "time" ) @@ -26,3 +28,17 @@ func CompactJSONString(js string) string { } 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 "" +} diff --git a/v3/internal/utilization/addresses.go b/v3/internal/utilization/addresses.go index 791b52726..aa2e7268d 100644 --- a/v3/internal/utilization/addresses.go +++ b/v3/internal/utilization/addresses.go @@ -47,8 +47,8 @@ func nonlocalIPAddressesByInterface() (map[string][]string, error) { // * The UDP connection interface is more likely to contain unique external IPs. func utilizationIPs() ([]string, error) { // Port choice designed to match - // https://source.datanerd.us/java-agent/java_agent/blob/master/newrelic-agent/src/main/java/com/newrelic/agent/config/Hostname.java#L110 - conn, err := net.Dial("udp", "newrelic.com:10002") + // 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 } diff --git a/v3/internal/utilization/azure.go b/v3/internal/utilization/azure.go index b2e1846be..5a747e2c1 100644 --- a/v3/internal/utilization/azure.go +++ b/v3/internal/utilization/azure.go @@ -28,7 +28,9 @@ func gatherAzure(util *Data, client *http.Client) error { 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 _, ok := err.(unexpectedAzureErr); ok { + // 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 diff --git a/v3/internal/utilization/utilization.go b/v3/internal/utilization/utilization.go index ad2a4ea5d..e9b5085d6 100644 --- a/v3/internal/utilization/utilization.go +++ b/v3/internal/utilization/utilization.go @@ -3,7 +3,6 @@ // Package utilization implements the Utilization spec, available at // https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md -// package utilization import ( @@ -84,6 +83,9 @@ type vendors struct { 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{} } diff --git a/v3/newrelic/app_run.go b/v3/newrelic/app_run.go index 3fa867a91..4136fb4ac 100644 --- a/v3/newrelic/app_run.go +++ b/v3/newrelic/app_run.go @@ -6,6 +6,7 @@ package newrelic import ( "encoding/json" "strings" + "sync" "time" "github.com/newrelic/go-agent/v3/internal" @@ -35,6 +36,11 @@ type appRun struct { // 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 ( @@ -43,10 +49,12 @@ const ( func newAppRun(config config, reply *internal.ConnectReply) *appRun { run := &appRun{ - Reply: reply, - AttributeConfig: createAttributeConfig(config, reply.SecurityPolicies.AttributesInclude.Enabled()), - Config: config, - rulesCache: newRulesCache(txnNameCacheLimit), + 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 @@ -54,16 +62,16 @@ func newAppRun(config config, reply *internal.ConnectReply) *appRun { // 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; nil != v { + if v := run.Reply.ServerSideConfig.TransactionTracerEnabled; v != nil { run.Config.TransactionTracer.Enabled = *v } - if v := run.Reply.ServerSideConfig.ErrorCollectorEnabled; nil != v { + if v := run.Reply.ServerSideConfig.ErrorCollectorEnabled; v != nil { run.Config.ErrorCollector.Enabled = *v } - if v := run.Reply.ServerSideConfig.CrossApplicationTracerEnabled; nil != v { + if v := run.Reply.ServerSideConfig.CrossApplicationTracerEnabled; v != nil { run.Config.CrossApplicationTracer.Enabled = *v } - if v := run.Reply.ServerSideConfig.TransactionTracerThreshold; nil != v { + if v := run.Reply.ServerSideConfig.TransactionTracerThreshold; v != nil { switch val := v.(type) { case float64: run.Config.TransactionTracer.Threshold.IsApdexFailing = false @@ -74,12 +82,30 @@ func newAppRun(config config, reply *internal.ConnectReply) *appRun { } } } - if v := run.Reply.ServerSideConfig.TransactionTracerStackTraceThreshold; nil != v { + if v := run.Reply.ServerSideConfig.TransactionTracerStackTraceThreshold; v != nil { run.Config.TransactionTracer.Segments.StackTraceThreshold = internal.FloatSecondsToDuration(*v) } - if v := run.Reply.ServerSideConfig.ErrorCollectorIgnoreStatusCodes; nil != 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 @@ -171,12 +197,15 @@ func (run *appRun) responseCodeIsError(code int) bool { if code < 400 && code >= 100 { return false } - for _, ignoreCode := range run.Config.ErrorCollector.IgnoreStatusCodes { - if code == ignoreCode { - return false - } - } - return true + 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 { @@ -219,7 +248,7 @@ func (run *appRun) LoggingConfig() (config loggingConfig) { // which will be the default or the user's configured size (if any), but // may be capped to the maximum allowed by the collector. func (run *appRun) MaxSpanEvents() int { - return run.limit(run.Config.DistributedTracer.ReservoirLimit, run.ptrSpanEvents) + return run.limit(internal.MaxSpanEvents, run.ptrSpanEvents) } func (run *appRun) limit(dflt int, field func() *uint) int { @@ -253,11 +282,11 @@ func (run *appRun) ReportPeriods() map[harvestTypes]time.Duration { } func (run *appRun) createTransactionName(input string, isWeb bool) string { - if name := run.rulesCache.find(input, isWeb); "" != name { + if name := run.rulesCache.find(input, isWeb); name != "" { return name } name := internal.CreateFullTxnName(input, run.Reply, isWeb) - if "" != name { + 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 diff --git a/v3/newrelic/app_run_test.go b/v3/newrelic/app_run_test.go index 57fe9c215..926f98168 100644 --- a/v3/newrelic/app_run_test.go +++ b/v3/newrelic/app_run_test.go @@ -42,7 +42,41 @@ func TestResponseCodeIsError(t *testing.T) { 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) { @@ -187,7 +221,7 @@ func TestZeroReportPeriod(t *testing.T) { maxCustomEvents: internal.MaxCustomEvents, maxLogEvents: internal.MaxLogEvents, maxErrorEvents: internal.MaxErrorEvents, - maxSpanEvents: defaultMaxSpanEvents, + maxSpanEvents: internal.MaxSpanEvents, periods: map[harvestTypes]time.Duration{ harvestTypesAll: 60 * time.Second, 0: 60 * time.Second, @@ -331,7 +365,7 @@ func TestConfigurableTxnEvents_withCollResponse(t *testing.T) { } result := newAppRun(config{Config: defaultConfig()}, h).MaxTxnEvents() if result != 15 { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", 15, result)) + t.Errorf("Unexpected max number of txn events, expected %d but got %d", 15, result) } } @@ -350,7 +384,7 @@ func TestConfigurableTxnEvents_notInCollResponse(t *testing.T) { cfg.TransactionEvents.MaxSamplesStored = expected result := newAppRun(cfg, reply).MaxTxnEvents() if result != expected { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", expected, result)) + t.Errorf("Unexpected max number of txn events, expected %d but got %d", expected, result) } } @@ -368,7 +402,7 @@ func TestConfigurableTxnEvents_configMoreThanMax(t *testing.T) { cfg.TransactionEvents.MaxSamplesStored = internal.MaxTxnEvents + 100 result := newAppRun(cfg, h).MaxTxnEvents() if result != internal.MaxTxnEvents { - t.Error(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", internal.MaxTxnEvents, result)) + t.Errorf(fmt.Sprintf("Unexpected max number of txn events, expected %d but got %d", internal.MaxTxnEvents, result)) } } diff --git a/v3/newrelic/application.go b/v3/newrelic/application.go index 6cc8cd446..d1ec4994c 100644 --- a/v3/newrelic/application.go +++ b/v3/newrelic/application.go @@ -15,9 +15,29 @@ type Application struct { 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 || app.app.run == nil { + return false + } + aiconf := app.app.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 +} +*/ + // StartTransaction begins a Transaction with the given name. func (app *Application) StartTransaction(name string, opts ...TraceOption) *Transaction { - if nil == app { + if app == nil { return nil } return app.app.StartTransaction(name, opts...) @@ -36,10 +56,7 @@ func (app *Application) StartTransaction(name string, opts ...TraceOption) *Tran // // An error is logged if eventType or params is invalid. func (app *Application) RecordCustomEvent(eventType string, params map[string]interface{}) { - if nil == app { - return - } - if nil == app.app { + if app == nil || app.app == nil { return } err := app.app.RecordCustomEvent(eventType, params) @@ -51,6 +68,69 @@ func (app *Application) RecordCustomEvent(eventType string, params map[string]in } } +// 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. @@ -59,10 +139,7 @@ func (app *Application) RecordCustomEvent(eventType string, params map[string]in // 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 nil == app { - return - } - if nil == app.app { + if app == nil || app.app == nil { return } err := app.app.RecordCustomMetric(name, value) @@ -83,10 +160,7 @@ func (app *Application) RecordCustomMetric(name string, value float64) { // as well as log metrics depending on how your application is // configured. func (app *Application) RecordLog(logEvent LogData) { - if nil == app { - return - } - if nil == app.app { + if app == nil || app.app == nil { return } err := app.app.RecordLog(&logEvent) @@ -114,9 +188,8 @@ func (app *Application) RecordLog(logEvent LogData) { // 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 nil == app { + if app == nil || app.app == nil { return nil } return app.app.WaitForConnection(timeout) @@ -132,12 +205,26 @@ func (app *Application) WaitForConnection(timeout time.Duration) error { // 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 nil == app { + 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, @@ -158,15 +245,15 @@ func newApplication(app *app) *Application { func NewApplication(opts ...ConfigOption) (*Application, error) { c := defaultConfig() for _, fn := range opts { - if nil != fn { + if fn != nil { fn(&c) - if nil != c.Error { + if c.Error != nil { return nil, c.Error } } } cfg, err := newInternalConfig(c, os.Getenv, os.Environ()) - if nil != err { + if err != nil { return nil, err } return newApplication(newApp(cfg)), nil diff --git a/v3/newrelic/attributes.go b/v3/newrelic/attributes.go index ca30d8f3c..2fd2f8d09 100644 --- a/v3/newrelic/attributes.go +++ b/v3/newrelic/attributes.go @@ -52,6 +52,12 @@ const ( 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: @@ -123,6 +129,8 @@ const ( 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" ) // Attributes destined for Span Events. These attributes appear only on Span diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index 28e8b0c37..430c5752a 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -5,10 +5,12 @@ package newrelic import ( "bytes" + "encoding/json" "fmt" "math" "net/http" "net/url" + "reflect" "sort" "strconv" "strings" @@ -19,6 +21,9 @@ const ( // 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 ( @@ -49,6 +54,7 @@ var ( AttributeAWSLambdaEventSourceARN: usualDests, AttributeMessageRoutingKey: usualDests, AttributeMessageQueueName: usualDests, + AttributeMessageHeaders: usualDests, AttributeMessageExchangeType: destNone, AttributeMessageReplyTo: destNone, AttributeMessageCorrelationID: destNone, @@ -56,6 +62,8 @@ var ( AttributeCodeNamespace: usualDests, AttributeCodeFilepath: usualDests, AttributeCodeLineno: usualDests, + AttributeUserID: usualDests, + AttributeLLM: usualDests, // Span specific attributes SpanAttributeDBStatement: usualDests, @@ -339,6 +347,13 @@ func truncateStringValueIfLong(val string) string { 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 { @@ -373,6 +388,36 @@ func validateUserAttribute(key string, val interface{}) (interface{}, error) { 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{ @@ -412,6 +457,9 @@ func addUserAttribute(a *attributes, key string, val interface{}, d destinationS 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 bool: if v { @@ -446,15 +494,26 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { case float64: w.floatField(key, v) default: - w.stringField(key, fmt.Sprintf("%T", v)) + // 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) { +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 { @@ -466,8 +525,15 @@ func agentAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet) { } } } - buf.WriteByte('}') + // 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{}) { diff --git a/v3/newrelic/code_level_metrics.go b/v3/newrelic/code_level_metrics.go index 561435e54..7fb9b4399 100644 --- a/v3/newrelic/code_level_metrics.go +++ b/v3/newrelic/code_level_metrics.go @@ -90,6 +90,7 @@ type traceOptSet struct { DemandCLM bool IgnoredPrefixes []string PathPrefixes []string + LocationCallback func() *CodeLocation } // @@ -106,12 +107,37 @@ type TraceOption func(*traceOptSet) // 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 @@ -385,6 +411,9 @@ func withPreparedOptions(newOptions *traceOptSet) TraceOption { 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 { @@ -542,9 +571,16 @@ func resolveCLMTraceOptions(options []TraceOption) *traceOptSet { 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 tOpts.LocationOverride != nil { - location = *tOpts.LocationOverride + if locationp != nil { + location = *locationp } else { pcs := make([]uintptr, 20) depth := runtime.Callers(2, pcs) diff --git a/v3/newrelic/collector.go b/v3/newrelic/collector.go index ea61e146c..791d55f48 100644 --- a/v3/newrelic/collector.go +++ b/v3/newrelic/collector.go @@ -9,10 +9,11 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strconv" + "sync" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/logger" @@ -52,9 +53,10 @@ type rpmCmd struct { // 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 + License string + Client *http.Client + Logger logger.Logger + GzipWriterPool *sync.Pool } // rpmResponse contains a NR endpoint response. @@ -62,32 +64,78 @@ type rpmControls struct { // 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 +// +// 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 +// +// 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 + err error disconnectSecurityPolicy bool // forceSaveHarvestData overrides the status code and forces a save of data forceSaveHarvestData bool } -func newRPMResponse(statusCode int) rpmResponse { - var err error +// 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 { - err = fmt.Errorf("response code: %d", statusCode) + resp.err = fmt.Errorf("response code: %d", statusCode) } - return rpmResponse{statusCode: statusCode, Err: err} + + 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. @@ -101,6 +149,10 @@ func (resp rpmResponse) IsRestartException() bool { 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 { @@ -136,9 +188,12 @@ func rpmURL(cmd rpmCmd, cs rpmControls) string { return u.String() } -func compress(b []byte) (*bytes.Buffer, error) { +func compress(b []byte, gzipWriterPool *sync.Pool) (*bytes.Buffer, error) { + w := gzipWriterPool.Get().(*gzip.Writer) + defer gzipWriterPool.Put(w) + var buf bytes.Buffer - w := gzip.NewWriter(&buf) + w.Reset(&buf) _, err := w.Write(b) w.Close() @@ -149,19 +204,19 @@ func compress(b []byte) (*bytes.Buffer, error) { return &buf, nil } -func collectorRequestInternal(url string, cmd rpmCmd, cs rpmControls) rpmResponse { - compressed, err := compress(cmd.Data) +func collectorRequestInternal(url string, cmd rpmCmd, cs rpmControls) *rpmResponse { + compressed, err := compress(cmd.Data, cs.GzipWriterPool) if nil != err { - return rpmResponse{Err: err} + return newRPMResponse(err) } if l := compressed.Len(); l > cmd.MaxPayloadSize { - return rpmResponse{Err: fmt.Errorf("Payload size for %s too large: %d greater than %d", cmd.Name, 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 rpmResponse{Err: err} + return newRPMResponse(err) } req.Header.Add("Accept-Encoding", "identity, deflate") @@ -174,32 +229,28 @@ func collectorRequestInternal(url string, cmd rpmCmd, cs rpmControls) rpmRespons resp, err := cs.Client.Do(req) if err != nil { - return rpmResponse{ - forceSaveHarvestData: true, - Err: err, - } + return newRPMResponse(err).ForceSaveHarvestData() } defer resp.Body.Close() - r := newRPMResponse(resp.StatusCode) + 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 := ioutil.ReadAll(resp.Body) - if nil == r.Err { - r.Err = err + body, err := io.ReadAll(resp.Body) + if r.GetError() == nil { + r.SetError(err) } - r.body = body - + r.AddBody(body) return r } // collectorRequest makes a request to New Relic. -func collectorRequest(cmd rpmCmd, cs rpmControls) rpmResponse { +func collectorRequest(cmd rpmCmd, cs rpmControls) *rpmResponse { url := rpmURL(cmd, cs) urlWithoutLicense := removeLicenseFromURL(url) @@ -214,7 +265,7 @@ func collectorRequest(cmd rpmCmd, cs rpmControls) rpmResponse { resp := collectorRequestInternal(url, cmd, cs) if cs.Logger.DebugEnabled() { - if err := resp.Err; err != nil { + if err := resp.GetError(); err != nil { cs.Logger.Debug("rpm failure", map[string]interface{}{ "command": cmd.Name, "url": urlWithoutLicense, @@ -261,13 +312,13 @@ var ( ) // connectAttempt tries to connect an application. -func connectAttempt(config config, cs rpmControls) (*internal.ConnectReply, rpmResponse) { +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, rpmResponse{Err: fmt.Errorf("unable to marshal preconnect data: %v", err)} + return nil, newRPMResponse(fmt.Errorf("unable to marshal preconnect data: %v", err)) } call := rpmCmd{ @@ -278,7 +329,7 @@ func connectAttempt(config config, cs rpmControls) (*internal.ConnectReply, rpmR } resp := collectorRequest(call, cs) - if nil != resp.Err { + if resp.GetError() != nil { return nil, resp } @@ -287,16 +338,17 @@ func connectAttempt(config config, cs rpmControls) (*internal.ConnectReply, rpmR } err = json.Unmarshal(resp.body, &preconnect) if nil != err { - // Certain security policy errors must be treated as a disconnect. - return nil, rpmResponse{ - Err: fmt.Errorf("unable to process preconnect reply: %v", err), - disconnectSecurityPolicy: internal.IsDisconnectSecurityPolicyError(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, rpmResponse{Err: fmt.Errorf("unable to create connect data: %v", err)} + return nil, newRPMResponse(fmt.Errorf("unable to create connect data: %v", err)) } call.Collector = preconnect.Preconnect.Collector @@ -304,19 +356,19 @@ func connectAttempt(config config, cs rpmControls) (*internal.ConnectReply, rpmR call.Name = cmdConnect resp = collectorRequest(call, cs) - if nil != resp.Err { + if resp.GetError() != nil { return nil, resp } reply, err := internal.UnmarshalConnectReply(resp.body, preconnect.Preconnect) if nil != err { - return nil, rpmResponse{Err: 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, rpmResponse{Err: errMissingAgentRunID} + return nil, newRPMResponse(errMissingAgentRunID) } return reply, resp diff --git a/v3/newrelic/collector_test.go b/v3/newrelic/collector_test.go index 7c9bb150f..f553258e9 100644 --- a/v3/newrelic/collector_test.go +++ b/v3/newrelic/collector_test.go @@ -4,13 +4,15 @@ package newrelic import ( + "compress/gzip" "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" + "sync" "testing" "time" @@ -18,6 +20,15 @@ import ( "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 @@ -57,18 +68,18 @@ func TestCollectorResponseCodeError(t *testing.T) { {code: 999999, success: false, disconnect: false, restart: false, saveHarvestData: false}, } for _, tc := range testcases { - resp := newRPMResponse(tc.code) - if tc.success != (nil == resp.Err) { - t.Error("error", tc.code, tc.success, resp.Err) + 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.Err) + t.Error("disconnect", tc.code, tc.disconnect, resp.GetError()) } if tc.restart != resp.IsRestartException() { - t.Error("restart", tc.code, tc.restart, resp.Err) + t.Error("restart", tc.code, tc.restart, resp.GetError()) } if tc.saveHarvestData != resp.ShouldSaveHarvestData() { - t.Error("save harvest data", tc.code, tc.saveHarvestData, resp.Err) + t.Error("save harvest data", tc.code, tc.saveHarvestData, resp.GetError()) } } } @@ -100,15 +111,20 @@ func TestCollectorRequest(t *testing.T) { testField("zip", r.Header.Get("zip"), "zap") return &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader("body")), + 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.Err { - t.Error(resp.Err) + if nil != resp.GetError() { + t.Error(resp.GetError()) } } @@ -126,15 +142,20 @@ func TestCollectorBadRequest(t *testing.T) { Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader("body")), + 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.Err { + if nil == resp.GetError() { t.Error("missing expected error") } } @@ -154,10 +175,15 @@ func TestCollectorTimeout(t *testing.T) { 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.Err { + if nil == resp.GetError() { t.Error("missing expected error") } if !resp.ShouldSaveHarvestData() { @@ -174,6 +200,11 @@ func TestUrl(t *testing.T) { License: "123abc", Client: nil, Logger: nil, + GzipWriterPool: &sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(io.Discard) + }, + }, } out := rpmURL(cmd, cs) @@ -201,7 +232,7 @@ const ( func makeResponse(code int, body string) *http.Response { return &http.Response{ StatusCode: code, - Body: ioutil.NopCloser(strings.NewReader(body)), + Body: io.NopCloser(strings.NewReader(body)), } } @@ -230,11 +261,16 @@ func (m connectMock) RoundTrip(r *http.Request) (*http.Response, error) { func (m connectMock) CancelRequest(req *http.Request) {} -func testConnectHelper(cm connectMock) (*internal.ConnectReply, rpmResponse) { +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) @@ -245,8 +281,8 @@ func TestConnectAttemptSuccess(t *testing.T) { redirect: endpointResult{response: makeResponse(200, redirectBody)}, connect: endpointResult{response: makeResponse(200, connectBody)}, }) - if nil == run || nil != resp.Err { - t.Fatal(run, resp.Err) + if nil == run || nil != resp.GetError() { + t.Fatal(run, resp.GetError()) } if run.Collector != "special_collector" { t.Error(run.Collector) @@ -264,7 +300,7 @@ func TestConnectClientError(t *testing.T) { if nil != run { t.Fatal(run) } - if resp.Err == nil { + if resp.GetError() == nil { t.Fatal("missing expected error") } } @@ -277,7 +313,7 @@ func TestConnectAttemptDisconnectOnRedirect(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { @@ -293,7 +329,7 @@ func TestConnectAttemptDisconnectOnConnect(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { @@ -309,7 +345,7 @@ func TestConnectAttemptBadSecurityPolicies(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsDisconnect() { @@ -325,7 +361,7 @@ func TestConnectAttemptInvalidJSON(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } } @@ -338,7 +374,7 @@ func TestConnectAttemptCollectorNotString(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } } @@ -351,7 +387,7 @@ func TestConnectAttempt401(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } if !resp.IsRestartException() { @@ -367,7 +403,7 @@ func TestConnectAttemptOtherReturnCode(t *testing.T) { if nil != run { t.Error(run) } - if nil == resp.Err { + if nil == resp.GetError() { t.Fatal("missing error") } } @@ -380,8 +416,8 @@ func TestConnectAttemptMissingRunID(t *testing.T) { if nil != run { t.Error(run) } - if errMissingAgentRunID != resp.Err { - t.Fatal("wrong error", resp.Err) + if errMissingAgentRunID != resp.GetError() { + t.Fatal("wrong error", resp.GetError()) } } @@ -403,9 +439,14 @@ func TestCollectorRequestRespectsMaxPayloadSize(t *testing.T) { }), }, Logger: logger.ShimLogger{IsDebugEnabled: true}, + GzipWriterPool: &sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(io.Discard) + }, + }, } resp := collectorRequest(cmd, cs) - if nil == resp.Err { + if nil == resp.GetError() { t.Error("response should have contained error") } if resp.ShouldSaveHarvestData() { @@ -434,18 +475,23 @@ func TestConnectReplyMaxPayloadSize(t *testing.T) { Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader(replyBody)), + 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.Err { - t.Error("resp returned unexpected error:", resp.Err) + 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", diff --git a/v3/newrelic/config.go b/v3/newrelic/config.go index 81cffdce7..b34537d68 100644 --- a/v3/newrelic/config.go +++ b/v3/newrelic/config.go @@ -101,6 +101,11 @@ type Config struct { // 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 // RecordPanics controls whether or not a deferred @@ -108,6 +113,24 @@ type Config struct { // 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. @@ -212,6 +235,17 @@ type Config struct { 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. @@ -293,6 +327,10 @@ type Config struct { 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. @@ -421,6 +459,8 @@ type Config struct { // 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 @@ -542,6 +582,15 @@ type ApplicationLogging 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. @@ -614,14 +663,14 @@ func defaultConfig() Config { c.ApplicationLogging.Forwarding.MaxSamplesStored = internal.MaxLogEvents 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 = defaultMaxSpanEvents + c.DistributedTracer.ReservoirLimit = internal.MaxSpanEvents c.SpanEvents.Enabled = true c.SpanEvents.Attributes.Enabled = true @@ -630,6 +679,7 @@ func defaultConfig() Config { 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 @@ -637,11 +687,14 @@ func defaultConfig() Config { 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 = false + c.CodeLevelMetrics.Enabled = true c.CodeLevelMetrics.RedactPathPrefixes = true c.CodeLevelMetrics.RedactIgnoredPrefixes = true c.CodeLevelMetrics.Scope = AllCLM @@ -679,16 +732,16 @@ func (c Config) validate() error { return errLicenseLen } } - if "" == c.AppName && c.Enabled && !c.ServerlessMode.Enabled { + if c.AppName == "" && c.Enabled && !c.ServerlessMode.Enabled { return errAppNameMissing } - if c.HighSecurity && "" != c.SecurityPoliciesToken { + if c.HighSecurity && c.SecurityPoliciesToken != "" { return errHighSecurityWithSecurityPolicies } if strings.Count(c.AppName, ";") >= appNameLimit { return errAppNameLimit } - if "" != c.InfiniteTracing.TraceObserver.Host && c.ServerlessMode.Enabled { + if c.InfiniteTracing.TraceObserver.Host != "" && c.ServerlessMode.Enabled { return errInfTracingServerless } @@ -697,7 +750,7 @@ func (c Config) validate() error { func (c Config) validateTraceObserverConfig() (*observerURL, error) { configHost := c.InfiniteTracing.TraceObserver.Host - if "" == configHost { + 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. @@ -766,7 +819,7 @@ func copyConfigReferenceFields(cfg Config) Config { cp.Labels[key] = val } } - if nil != cfg.ErrorCollector.IgnoreStatusCodes { + if cfg.ErrorCollector.IgnoreStatusCodes != nil { ignored := make([]int, len(cfg.ErrorCollector.IgnoreStatusCodes)) copy(ignored, cfg.ErrorCollector.IgnoreStatusCodes) cp.ErrorCollector.IgnoreStatusCodes = ignored @@ -1030,7 +1083,7 @@ var ( ) func (c config) preconnectHost() string { - if "" != c.Host { + if c.Host != "" { return c.Host } m := preconnectRegionLicenseRegex.FindStringSubmatch(c.License) diff --git a/v3/newrelic/config_options.go b/v3/newrelic/config_options.go index aea7d4026..8c3daf6f7 100644 --- a/v3/newrelic/config_options.go +++ b/v3/newrelic/config_options.go @@ -60,6 +60,13 @@ func ConfigDistributedTracerReservoirLimit(limit int) ConfigOption { return func(cfg *Config) { cfg.DistributedTracer.ReservoirLimit = limit } } +// 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 { @@ -68,6 +75,14 @@ func ConfigCodeLevelMetricsEnabled(enabled bool) ConfigOption { } } +// 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 @@ -228,6 +243,21 @@ func ConfigAppLogDecoratingEnabled(enabled bool) ConfigOption { } } +// 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 @@ -272,6 +302,13 @@ 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 { @@ -288,6 +325,17 @@ func ConfigModuleDependencyMetricsIgnoredPrefixes(prefix ...string) ConfigOption } } +// 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 @@ -344,6 +392,9 @@ func ConfigDebugLogger(w io.Writer) ConfigOption { // 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_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. @@ -407,6 +458,9 @@ func configFromEnvironment(getenv func(string) string) ConfigOption { assignInt(&cfg.ApplicationLogging.Forwarding.MaxSamplesStored, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED") 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") if env := getenv("NEW_RELIC_LABELS"); env != "" { if labels := getLabels(getenv("NEW_RELIC_LABELS")); len(labels) > 0 { diff --git a/v3/newrelic/config_test.go b/v3/newrelic/config_test.go index 056889f07..6ce06a6e6 100644 --- a/v3/newrelic/config_test.go +++ b/v3/newrelic/config_test.go @@ -86,6 +86,7 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { cfg.License = "0123456789012345678901234567890123456789" cfg.Labels["zip"] = "zap" 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") @@ -129,6 +130,15 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "agent_version":"0.2.2", "host":"my-hostname", "settings":{ + "AIMonitoring": { + "Enabled": false, + "RecordContent": { + "Enabled": true + }, + "Streaming": { + "Enabled": true + } + }, "AppName":"my appname", "ApplicationLogging": { "Enabled": true, @@ -141,6 +151,9 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { }, "Metrics": { "Enabled": true + }, + "ZapLogger": { + "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":["2"],"Include":["1"]}, @@ -148,7 +161,7 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "Attributes":{"Enabled":false,"Exclude":["10"],"Include":["9"]}, "Enabled":true }, - "CodeLevelMetrics":{"Enabled":false,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, + "CodeLevelMetrics":{"Enabled":true,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, "CrossApplicationTracer":{"Enabled":false}, "CustomInsightsEvents":{ "Enabled":true, @@ -158,18 +171,20 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "DatabaseNameReporting":{"Enabled":true}, "InstanceReporting":{"Enabled":true}, "QueryParameters":{"Enabled":true}, + "RawQuery":{"Enabled":false}, "SlowQuery":{ "Enabled":true, "Threshold":10000000 } }, - "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":2000}, + "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":%d}, "Enabled":true, "Error":null, "ErrorCollector":{ "Attributes":{"Enabled":true,"Exclude":["6"],"Include":["5"]}, "CaptureEvents":true, "Enabled":true, + "ExpectStatusCodes":[500], "IgnoreStatusCodes":[0,5,404,405], "RecordPanics":false }, @@ -272,10 +287,10 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "custom_event_data": %d, "log_event_data": %d, "error_event_data": 100, - "span_event_data": 2000 + "span_event_data": %d } } - }]`, internal.MaxLogEvents, internal.MaxCustomEvents, internal.MaxTxnEvents, internal.MaxCustomEvents, internal.MaxTxnEvents)) + }]`, internal.MaxLogEvents, internal.MaxCustomEvents, internal.MaxSpanEvents, internal.MaxTxnEvents, internal.MaxCustomEvents, internal.MaxTxnEvents, internal.MaxSpanEvents)) securityPoliciesInput := []byte(`{ "record_sql": { "enabled": false, "required": false }, @@ -323,6 +338,15 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { "agent_version":"0.2.2", "host":"my-hostname", "settings":{ + "AIMonitoring": { + "Enabled": false, + "RecordContent": { + "Enabled": true + }, + "Streaming": { + "Enabled": true + } + }, "AppName":"my appname", "ApplicationLogging": { "Enabled": true, @@ -335,6 +359,9 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { }, "Metrics": { "Enabled": true + }, + "ZapLogger": { + "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, @@ -346,7 +373,7 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { }, "Enabled":true }, - "CodeLevelMetrics":{"Enabled":false,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, + "CodeLevelMetrics":{"Enabled":true,"IgnoredPrefix":"","IgnoredPrefixes":null,"PathPrefix":"","PathPrefixes":null,"RedactIgnoredPrefixes":true,"RedactPathPrefixes":true,"Scope":"all"}, "CrossApplicationTracer":{"Enabled":false}, "CustomInsightsEvents":{ "Enabled":true, @@ -356,18 +383,20 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { "DatabaseNameReporting":{"Enabled":true}, "InstanceReporting":{"Enabled":true}, "QueryParameters":{"Enabled":true}, + "RawQuery":{"Enabled":false}, "SlowQuery":{ "Enabled":true, "Threshold":10000000 } }, - "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":2000}, + "DistributedTracer":{"Enabled":true,"ExcludeNewRelicHeader":false,"ReservoirLimit":%d}, "Enabled":true, "Error":null, "ErrorCollector":{ "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, "CaptureEvents":true, "Enabled":true, + "ExpectStatusCodes":null, "IgnoreStatusCodes":null, "RecordPanics":false }, @@ -458,10 +487,10 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { "custom_event_data": %d, "log_event_data": %d, "error_event_data": 100, - "span_event_data": 2000 + "span_event_data": %d } } - }]`, internal.MaxLogEvents, internal.MaxCustomEvents, internal.MaxTxnEvents, internal.MaxCustomEvents, internal.MaxTxnEvents)) + }]`, internal.MaxLogEvents, internal.MaxCustomEvents, 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) @@ -480,7 +509,7 @@ func TestValidate(t *testing.T) { AppName: "my app", Enabled: true, } - if err := c.validate(); nil != err { + if err := c.validate(); err != nil { t.Error(err) } c = Config{ @@ -496,7 +525,7 @@ func TestValidate(t *testing.T) { AppName: "my app", Enabled: false, } - if err := c.validate(); nil != err { + if err := c.validate(); err != nil { t.Error(err) } c = Config{ @@ -676,11 +705,11 @@ func TestPreconnectHostCrossAgent(t *testing.T) { for _, tc := range testcases { // mimic file/environment precedence of other agents configKey := tc.ConfigFileKey - if "" != tc.EnvKey { + if tc.EnvKey != "" { configKey = tc.EnvKey } overrideHost := tc.ConfigOverrideHost - if "" != tc.EnvOverrideHost { + if tc.EnvOverrideHost != "" { overrideHost = tc.EnvOverrideHost } diff --git a/v3/newrelic/cross_process_http.go b/v3/newrelic/cross_process_http.go index b05d3267f..050bff1f6 100644 --- a/v3/newrelic/cross_process_http.go +++ b/v3/newrelic/cross_process_http.go @@ -43,9 +43,10 @@ func httpHeaderToMetadata(header http.Header) crossProcessMetadata { } return crossProcessMetadata{ - ID: header.Get(cat.NewRelicIDName), - TxnData: header.Get(cat.NewRelicTxnName), - Synthetics: header.Get(cat.NewRelicSyntheticsName), + ID: header.Get(cat.NewRelicIDName), + TxnData: header.Get(cat.NewRelicTxnName), + Synthetics: header.Get(cat.NewRelicSyntheticsName), + SyntheticsInfo: header.Get(cat.NewRelicSyntheticsInfo), } } @@ -64,6 +65,11 @@ func metadataToHTTPHeader(metadata crossProcessMetadata) http.Header { 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 diff --git a/v3/newrelic/custom_event.go b/v3/newrelic/custom_event.go index a1aacb8f3..80aa08812 100644 --- a/v3/newrelic/custom_event.go +++ b/v3/newrelic/custom_event.go @@ -100,6 +100,32 @@ func createCustomEvent(eventType string, params map[string]interface{}, now time }, 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) diff --git a/v3/newrelic/environment_test.go b/v3/newrelic/environment_test.go index bc3c88d7e..d19fb1996 100644 --- a/v3/newrelic/environment_test.go +++ b/v3/newrelic/environment_test.go @@ -73,9 +73,9 @@ func TestModuleDependency(t *testing.T) { // of modules to at least check that the various options work. expectedModules := make(map[string]*debug.Module) mockedModuleList := []*debug.Module{ - &debug.Module{Path: "example/path/to/module", Version: "v1.2.3"}, - &debug.Module{Path: "github.com/another/module", Version: "v0.1.2"}, - &debug.Module{Path: "some/development/module", Version: "(develop)"}, + {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 diff --git a/v3/newrelic/error_events.go b/v3/newrelic/error_events.go index 90419747c..6087865d8 100644 --- a/v3/newrelic/error_events.go +++ b/v3/newrelic/error_events.go @@ -31,6 +31,9 @@ func (e *errorEvent) WriteJSON(buf *bytes.Buffer) { if e.SpanID != "" { w.stringField("spanId", e.SpanID) } + if e.Expect { + w.boolField(expectErrorAttr, true) + } sharedTransactionIntrinsics(&e.txnEvent, &w) sharedBetterCATIntrinsics(&e.txnEvent, &w) @@ -39,7 +42,13 @@ func (e *errorEvent) WriteJSON(buf *bytes.Buffer) { buf.WriteByte(',') userAttributesJSON(e.Attrs, buf, destError, e.errorData.ExtraAttributes) buf.WriteByte(',') - agentAttributesJSON(e.Attrs, buf, destError) + + if e.ErrorGroup != "" { + agentAttributesJSON(e.Attrs, buf, destError, map[string]string{AttributeErrorGroupName: e.ErrorGroup}) + } else { + agentAttributesJSON(e.Attrs, buf, destError) + } + buf.WriteByte(']') } diff --git a/v3/newrelic/error_group.go b/v3/newrelic/error_group.go new file mode 100644 index 000000000..4a25a8aa6 --- /dev/null +++ b/v3/newrelic/error_group.go @@ -0,0 +1,138 @@ +package 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 diff --git a/v3/newrelic/errors_from_internal.go b/v3/newrelic/errors_from_internal.go index 9b174f3f0..fdb439366 100644 --- a/v3/newrelic/errors_from_internal.go +++ b/v3/newrelic/errors_from_internal.go @@ -57,7 +57,9 @@ func txnErrorFromResponseCode(now time.Time, code int) errorData { type errorData struct { When time.Time Stack stackTrace + RawError error ExtraAttributes map[string]interface{} + ErrorGroup string Msg string Klass string SpanID string @@ -122,6 +124,8 @@ func (h *tracedError) WriteJSON(buf *bytes.Buffer) { h.Stack.WriteJSON(buf) } buf.WriteByte('}') + buf.WriteByte(',') + jsonx.AppendString(buf, h.txnEvent.TxnID) buf.WriteByte(']') } @@ -140,11 +144,13 @@ func newHarvestErrors(max int) harvestErrors { } // mergeTxnErrors merges a transaction's errors into the harvest's errors. -func mergeTxnErrors(errors *harvestErrors, errs txnErrors, txnEvent txnEvent) { +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, @@ -178,3 +184,67 @@ 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 +} diff --git a/v3/newrelic/errors_test.go b/v3/newrelic/errors_test.go index 9ab80bb8e..6766276e9 100644 --- a/v3/newrelic/errors_test.go +++ b/v3/newrelic/errors_test.go @@ -27,6 +27,48 @@ func testExpectedJSON(t testing.TB, expect string, actual string) { } } +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{ @@ -38,6 +80,7 @@ func TestErrorTraceMarshal(t *testing.T) { txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: nil, + TxnID: "txn-guid-id", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", @@ -69,7 +112,8 @@ func TestErrorTraceMarshal(t *testing.T) { "sampled":false }, "stack_trace":[] - } + }, + "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } @@ -89,6 +133,7 @@ func TestErrorTraceMarshalOldCAT(t *testing.T) { Enabled: false, }, TotalTime: 2 * time.Second, + TxnID: "txn-guid-id", }, } js, err := json.Marshal(he) @@ -109,7 +154,8 @@ func TestErrorTraceMarshalOldCAT(t *testing.T) { "totalTime":2 }, "stack_trace":[] - } + }, + "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } @@ -135,11 +181,13 @@ func TestErrorTraceAttributes(t *testing.T) { txnEvent: txnEvent{ FinalName: "my_txn_name", Attrs: attr, + TxnID: "txn-id", + BetterCAT: betterCAT{ Enabled: true, - TxnID: "txn-id", Priority: 0.5, TraceID: "trace-id", + TxnID: "txn-id", }, TotalTime: 2 * time.Second, }, @@ -164,7 +212,8 @@ func TestErrorTraceAttributes(t *testing.T) { "priority":0.500000, "sampled":false } - } + }, + "txn-id" ]` testExpectedJSON(t, expect, string(js)) } @@ -188,6 +237,7 @@ func TestErrorTraceAttributesOldCAT(t *testing.T) { Klass: "my_class", }, txnEvent: txnEvent{ + TxnID: "txn-guid-id", FinalName: "my_txn_name", Attrs: attr, BetterCAT: betterCAT{ @@ -212,7 +262,8 @@ func TestErrorTraceAttributesOldCAT(t *testing.T) { "intrinsics":{ "totalTime":2 } - } + }, + "txn-guid-id" ]` testExpectedJSON(t, expect, string(js)) } @@ -231,6 +282,8 @@ func TestErrorsLifecycle(t *testing.T) { mergeTxnErrors(&he, ers, txnEvent{ FinalName: "txnName", Attrs: nil, + TxnID: "txn-id", + BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", @@ -238,7 +291,7 @@ func TestErrorsLifecycle(t *testing.T) { Priority: 0.5, }, TotalTime: 2 * time.Second, - }) + }, nil) js, err := he.Data("agentRunID", time.Now()) if nil != err { t.Error(err) @@ -257,12 +310,13 @@ func TestErrorsLifecycle(t *testing.T) { "userAttributes":{}, "intrinsics":{ "totalTime":2, - "guid":"txn-id", + "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } - } + }, + "txn-id" ], [ 1.41713646e+12, @@ -274,12 +328,13 @@ func TestErrorsLifecycle(t *testing.T) { "userAttributes":{}, "intrinsics":{ "totalTime":2, - "guid":"txn-id", + "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } - } + }, + "txn-id" ], [ 1.41713646e+12, @@ -291,12 +346,13 @@ func TestErrorsLifecycle(t *testing.T) { "userAttributes":{}, "intrinsics":{ "totalTime":2, - "guid":"txn-id", + "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } - } + }, + "txn-id" ], [ 1.41713646e+12, @@ -308,17 +364,18 @@ func TestErrorsLifecycle(t *testing.T) { "userAttributes":{}, "intrinsics":{ "totalTime":2, - "guid":"txn-id", + "guid":"txn-id", "traceId":"trace-id", "priority":0.500000, "sampled":false } - } + }, + "txn-id" ] ] ]`) if string(js) != expect { - t.Error(string(js), expect) + t.Error(string(js), "expect: ", expect) } } @@ -344,7 +401,7 @@ func BenchmarkErrorsJSON(b *testing.B) { mergeTxnErrors(&he, ers, txnEvent{ FinalName: "WebTransaction/Go/hello", Attrs: attr, - }) + }, nil) b.ReportAllocs() b.ResetTimer() diff --git a/v3/newrelic/examples_test.go b/v3/newrelic/examples_test.go index 1fc800e56..96458486a 100644 --- a/v3/newrelic/examples_test.go +++ b/v3/newrelic/examples_test.go @@ -22,6 +22,7 @@ func Example() { 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 { @@ -80,7 +81,7 @@ func ExampleNewRoundTripper() { // requests done by this client with external segments. client.Transport = newrelic.NewRoundTripper(client.Transport) - request, _ := http.NewRequest("GET", "http://example.com", nil) + 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. @@ -159,7 +160,7 @@ func ExampleError() { func ExampleExternalSegment() { txn := currentTransaction() client := &http.Client{} - request, _ := http.NewRequest("GET", "http://www.example.com", nil) + request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) response, _ := client.Do(request) segment.Response = response @@ -185,7 +186,7 @@ func ExampleExternalSegment_url() { func ExampleStartExternalSegment() { txn := currentTransaction() client := &http.Client{} - request, _ := http.NewRequest("GET", "http://www.example.com", nil) + request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) response, _ := client.Do(request) segment.Response = response @@ -194,7 +195,7 @@ func ExampleStartExternalSegment() { func ExampleStartExternalSegment_context() { txn := currentTransaction() - request, _ := http.NewRequest("GET", "http://www.example.com", nil) + 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. @@ -213,7 +214,7 @@ func doSendRequest(*http.Request) int { return 418 } // http.Response and still want to record the response status code. func ExampleExternalSegment_SetStatusCode() { txn := currentTransaction() - request, _ := http.NewRequest("GET", "http://www.example.com", nil) + request, _ := http.NewRequest("GET", "https://www.example.com", nil) segment := newrelic.StartExternalSegment(txn, request) statusCode := doSendRequest(request) segment.SetStatusCode(statusCode) @@ -233,7 +234,7 @@ func ExampleTransaction_SetWebRequest() { func ExampleTransaction_SetWebRequestHTTP() { app := getApp() - inboundRequest, _ := http.NewRequest("GET", "http://example.com", nil) + 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. diff --git a/v3/newrelic/expect_implementation.go b/v3/newrelic/expect_implementation.go index 970ad4d07..122d588ff 100644 --- a/v3/newrelic/expect_implementation.go +++ b/v3/newrelic/expect_implementation.go @@ -249,8 +249,75 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog v.Error(fmt.Sprintf("unexpected log timestamp: got %d, want %d", actual.timestamp, want.Timestamp)) return } + + if actual.attributes != nil && want.Attributes != nil { + for k, val := range want.Attributes { + actualVal, actualOk := actual.attributes[k] + if !actualOk { + v.Error(fmt.Sprintf("expected log attribute for key %v is missing", k)) + return + } + + // Check if both values are maps, and if so, compare them recursively + if expectedMap, ok := val.(map[string]interface{}); ok { + if actualMap, ok := actualVal.(map[string]interface{}); ok { + if !expectLogEventAttributesMaps(expectedMap, actualMap) { + v.Error(fmt.Sprintf("unexpected log attribute for key %v: got %v, want %v", k, actualMap, expectedMap)) + return + } + } else { + v.Error(fmt.Sprintf("actual value for key %v is not a map", k)) + return + } + } + } + } + } +// Helper function that compares two maps for equality. This is used to compare the attribute fields of log events expected vs received +func expectLogEventAttributesMaps(a, b map[string]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v := range a { + if bv, ok := b[k]; !ok { + return false + } else { + switch v := v.(type) { + case float64: + if bv, ok := bv.(float64); !ok || v != bv { + return false + } + + case int: + if bv, ok := bv.(int); !ok || v != bv { + return false + } + case time.Duration: + if bv, ok := bv.(time.Duration); ok { + return v == bv + } + case string: + if bv, ok := bv.(string); !ok || v != bv { + return false + } + case int64: + if bv, ok := bv.(int64); !ok || v != bv { + return false + } + // if the type of the field is a map, recursively compare the maps + case map[string]interface{}: + if bv, ok := bv.(map[string]interface{}); !ok || !expectLogEventAttributesMaps(v, bv) { + return false + } + default: + return false + } + } + } + return true +} func expectEvent(v internal.Validator, e json.Marshaler, expect internal.WantEvent) { js, err := e.MarshalJSON() if nil != err { diff --git a/v3/newrelic/harvest.go b/v3/newrelic/harvest.go index 72159e49a..fb6f368d9 100644 --- a/v3/newrelic/harvest.go +++ b/v3/newrelic/harvest.go @@ -349,7 +349,7 @@ var ( dfltHarvestCfgr = harvestConfig{ ReportPeriods: map[harvestTypes]time.Duration{harvestTypesAll: fixedHarvestPeriod}, MaxTxnEvents: internal.MaxTxnEvents, - MaxSpanEvents: defaultMaxSpanEvents, + MaxSpanEvents: internal.MaxSpanEvents, MaxCustomEvents: internal.MaxCustomEvents, MaxErrorEvents: internal.MaxErrorEvents, LoggingConfig: loggingConfig{ diff --git a/v3/newrelic/harvest_test.go b/v3/newrelic/harvest_test.go index b175e2232..9e9f00314 100644 --- a/v3/newrelic/harvest_test.go +++ b/v3/newrelic/harvest_test.go @@ -167,7 +167,7 @@ func TestCreateFinalMetrics(t *testing.T) { {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, 2000, 2000, 2000, 2000, 2000 * 2000}}, + {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}}, @@ -315,6 +315,7 @@ func TestHarvestLogEventsReady(t *testing.T) { }) logEvent := logEvent{ + nil, 0.5, 123456, "INFO", @@ -415,7 +416,7 @@ func TestHarvestErrorEventsReady(t *testing.T) { }) h.ErrorEvents.Add(&errorEvent{ errorData: errorData{Klass: "klass", Msg: "msg", When: time.Now()}, - txnEvent: txnEvent{FinalName: "finalName", Duration: 1 * time.Second}, + 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) @@ -509,7 +510,7 @@ func TestHarvestMetricsTracesReady(t *testing.T) { ers := newTxnErrors(10) ers.Add(errorData{When: time.Now(), Msg: "msg", Klass: "klass", Stack: getStackTrace()}) - mergeTxnErrors(&h.ErrorTraces, ers, txnEvent{FinalName: "finalName", Attrs: nil}) + mergeTxnErrors(&h.ErrorTraces, ers, txnEvent{FinalName: "finalName", Attrs: nil}, nil) h.TxnTraces.Witness(harvestTrace{ txnEvent: txnEvent{ @@ -544,6 +545,7 @@ func TestHarvestMetricsTracesReady(t *testing.T) { TxnName: "finalName", Msg: "msg", Klass: "klass", + GUID: "error-guid-id", }}) expectErrors(t, h.ErrorTraces, []internal.WantError{}) @@ -575,6 +577,7 @@ func TestMergeFailedHarvest(t *testing.T) { }, 0) logEvent := logEvent{ + nil, 0.5, 123456, "INFO", @@ -612,7 +615,7 @@ func TestMergeFailedHarvest(t *testing.T) { mergeTxnErrors(&h.ErrorTraces, ers, txnEvent{ FinalName: "finalName", Attrs: nil, - }) + }, nil) h.SpanEvents.addEventPopulated(&sampleSpanEvent) if start1 != h.Metrics.metricPeriodStart { @@ -997,7 +1000,7 @@ func TestNewHarvestSetsDefaultValues(t *testing.T) { if cp := h.ErrorEvents.capacity(); cp != internal.MaxErrorEvents { t.Error("wrong error event capacity", cp) } - if cp := h.SpanEvents.capacity(); cp != defaultMaxSpanEvents { + if cp := h.SpanEvents.capacity(); cp != internal.MaxSpanEvents { t.Error("wrong span event capacity", cp) } } diff --git a/v3/newrelic/instrumentation.go b/v3/newrelic/instrumentation.go index 5765d478e..efc84d22c 100644 --- a/v3/newrelic/instrumentation.go +++ b/v3/newrelic/instrumentation.go @@ -5,6 +5,8 @@ package newrelic import ( "net/http" + + "github.com/newrelic/go-agent/v3/internal" ) // instrumentation.go contains helpers built on the lower level api. @@ -12,11 +14,11 @@ import ( // WrapHandle instruments http.Handler handlers with Transactions. To // instrument this code: // -// http.Handle("/foo", myHandler) +// http.Handle("/foo", myHandler) // // Perform this replacement: // -// http.Handle(newrelic.WrapHandle(app, "/foo", myHandler)) +// 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: @@ -42,6 +44,10 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options // 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 @@ -66,7 +72,9 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options txn := app.StartTransaction(r.Method+" "+pattern, txnOptionList...) defer txn.End() - + if IsSecurityAgentPresent() { + txn.SetCsecAttributes(AttributeCsecRoute, pattern) + } w = txn.SetWebResponse(w) txn.SetWebRequestHTTP(r) @@ -76,6 +84,39 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options }) } +// 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 && app.app.run != nil && app.app.run.Config.CodeLevelMetrics.Enabled { + tOptions = resolveCLMTraceOptions(options) + if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || app.app.run.Config.CodeLevelMetrics.Scope == 0 || (app.app.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: // @@ -111,6 +152,22 @@ func WrapHandleFunc(app *Application, pattern string, handler func(http.Response 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 diff --git a/v3/newrelic/internal_app.go b/v3/newrelic/internal_app.go index 1d8b9d474..54b7b4fdc 100644 --- a/v3/newrelic/internal_app.go +++ b/v3/newrelic/internal_app.go @@ -4,6 +4,7 @@ package newrelic import ( + "compress/gzip" "errors" "fmt" "io" @@ -62,6 +63,9 @@ type app struct { // (disconnect, license exception, shutdown). err error + // registered callback functions + llmTokenCountCallback func(string, string) int + serverless *serverlessHarvest } @@ -111,16 +115,16 @@ func (app *app) doHarvest(h *harvest, harvestStart time.Time, run *appRun) { if resp.IsDisconnect() || resp.IsRestartException() { select { - case app.collectorErrorChan <- resp: + case app.collectorErrorChan <- *resp: case <-app.shutdownStarted: } return } - if resp.Err != nil { + if resp.GetError() != nil { app.Warn("harvest failure", map[string]interface{}{ "cmd": cmd, - "error": resp.Err.Error(), + "error": resp.GetError().Error(), "retain_data": resp.ShouldSaveHarvestData(), }) } @@ -146,15 +150,15 @@ func (app *app) connectRoutine() { if resp.IsDisconnect() { select { - case app.collectorErrorChan <- resp: + case app.collectorErrorChan <- *resp: case <-app.shutdownStarted: } return } - if nil != resp.Err { + if nil != resp.GetError() { app.Warn("application connect failure", map[string]interface{}{ - "error": resp.Err.Error(), + "error": resp.GetError().Error(), }) } @@ -251,7 +255,7 @@ func (app *app) process() { // Remove the run before merging any final data to // ensure a bounded number of receives from dataChan. - app.setState(nil, errors.New("application shut down")) + app.setState(nil, errApplicationShutDown) if obs := app.getObserver(); obs != nil { if err := obs.shutdown(timeout); err != nil { @@ -277,6 +281,7 @@ func (app *app) process() { close(app.shutdownComplete) app.setObserver(nil) + secureAgent.DeactivateSecurity() return case resp := <-app.collectorErrorChan: run = nil @@ -284,10 +289,11 @@ func (app *app) process() { app.setState(nil, nil) if resp.IsDisconnect() { - app.setState(nil, resp.Err) + 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, @@ -320,6 +326,7 @@ func (app *app) process() { "run": run.Reply.RunID.String(), }) processConnectMessages(run, app) + secureAgent.RefreshState(getLinkedMetaData(app)) } } } @@ -433,6 +440,11 @@ func newApp(c config) *app { Timeout: collectorTimeout, }, Logger: c.Logger, + GzipWriterPool: &sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(io.Discard) + }, + }, }, } @@ -529,13 +541,18 @@ 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 } @@ -544,7 +561,11 @@ func (app *app) RecordCustomEvent(eventType string, params map[string]interface{ return errCustomEventsDisabled } - event, e := createCustomEvent(eventType, params, time.Now()) + 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 } @@ -584,7 +605,7 @@ func (app *app) RecordCustomMetric(name string, value float64) error { if math.IsInf(value, 0) { return errMetricInf } - if "" == name { + if name == "" { return errMetricNameEmpty } run, _ := app.getState() @@ -632,7 +653,7 @@ func (app *app) Consume(id internal.AgentRunID, data harvestable) { return } - if "" == id { + if id == "" { return } diff --git a/v3/newrelic/internal_app_test.go b/v3/newrelic/internal_app_test.go index 5e1b342a3..9857596c3 100644 --- a/v3/newrelic/internal_app_test.go +++ b/v3/newrelic/internal_app_test.go @@ -95,6 +95,7 @@ func newTestApp(replyfn func(*internal.ConnectReply), cfgFn ...ConfigOption) exp }, ConfigAppName(sampleAppName), ConfigLicense(testLicenseKey), + ConfigCodeLevelMetricsEnabled(false), ) app, err := NewApplication(cfgFn...) diff --git a/v3/newrelic/internal_benchmark_test.go b/v3/newrelic/internal_benchmark_test.go index 22a5371cd..0d3019ea5 100644 --- a/v3/newrelic/internal_benchmark_test.go +++ b/v3/newrelic/internal_benchmark_test.go @@ -55,6 +55,7 @@ func BenchmarkTraceSegmentWithDefer(b *testing.B) { ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) @@ -75,6 +76,7 @@ func BenchmarkTraceSegmentNoDefer(b *testing.B) { ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) @@ -96,6 +98,7 @@ func BenchmarkTraceSegmentZeroSegmentThreshold(b *testing.B) { ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), func(cfg *Config) { cfg.TransactionTracer.Segments.Threshold = 0 }, @@ -120,6 +123,7 @@ func BenchmarkDatastoreSegment(b *testing.B) { ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) @@ -146,6 +150,7 @@ func BenchmarkExternalSegment(b *testing.B) { ConfigAppName("my app"), ConfigLicense(sampleLicense), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), ) if nil != err { b.Fatal(err) diff --git a/v3/newrelic/internal_response_writer.go b/v3/newrelic/internal_response_writer.go index b363e9166..ba5427c79 100644 --- a/v3/newrelic/internal_response_writer.go +++ b/v3/newrelic/internal_response_writer.go @@ -30,6 +30,9 @@ func (rw *replacementResponseWriter) Write(b []byte) (n int, err error) { headersJustWritten(rw.thd, http.StatusOK, hdr) + if IsSecurityAgentPresent() { + secureAgent.SendEvent("INBOUND_WRITE", string(b), hdr) + } return } @@ -41,6 +44,9 @@ func (rw *replacementResponseWriter) WriteHeader(code int) { rw.original.WriteHeader(code) headersJustWritten(rw.thd, code, hdr) + if IsSecurityAgentPresent() { + secureAgent.SendEvent("INBOUND_RESPONSE_CODE", code) + } } func (rw *replacementResponseWriter) CloseNotify() <-chan bool { diff --git a/v3/newrelic/internal_test.go b/v3/newrelic/internal_test.go index 541a5e85a..a1deb7878 100644 --- a/v3/newrelic/internal_test.go +++ b/v3/newrelic/internal_test.go @@ -5,6 +5,8 @@ package newrelic import ( "encoding/json" + "errors" + "fmt" "math" "net/http" "net/http/httptest" @@ -137,9 +139,11 @@ type compatibleResponseRecorder struct { } func newCompatibleResponseRecorder() *compatibleResponseRecorder { - return &compatibleResponseRecorder{ + recorder := compatibleResponseRecorder{ ResponseRecorder: httptest.NewRecorder(), } + + return &recorder } func (rw *compatibleResponseRecorder) Header() http.Header { @@ -203,6 +207,7 @@ func TestNewApplicationNil(t *testing.T) { ConfigAppName("appname"), ConfigLicense("wrong length"), ConfigEnabled(false), + ConfigCodeLevelMetricsEnabled(false), ) if nil == err { t.Error("error expected when license key is short") @@ -262,6 +267,7 @@ func testApp(replyfn func(*internal.ConnectReply), cfgfn func(*Config), t testin app, err := NewApplication( ConfigAppName("my app"), ConfigLicense(testLicenseKey), + ConfigCodeLevelMetricsEnabled(false), cfgfn, func(cfg *Config) { cfg.Logger = lg @@ -283,6 +289,26 @@ func testApp(replyfn func(*internal.ConnectReply), cfgfn func(*Config), t testin } } +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) @@ -491,6 +517,84 @@ func TestSetName(t *testing.T) { 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() @@ -548,6 +652,51 @@ func TestPanicError(t *testing.T) { 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) @@ -656,6 +805,116 @@ func TestResponseCodeError(t *testing.T) { 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() @@ -678,8 +937,7 @@ func TestResponseCode404Filtered(t *testing.T) { func TestResponseCodeCustomFilter(t *testing.T) { cfgFn := func(cfg *Config) { - cfg.ErrorCollector.IgnoreStatusCodes = - append(cfg.ErrorCollector.IgnoreStatusCodes, 405) + cfg.ErrorCollector.IgnoreStatusCodes = []int{405} cfg.DistributedTracer.Enabled = false } app := testApp(nil, cfgFn, t) @@ -790,7 +1048,7 @@ func TestResponseCodeAfterWrite(t *testing.T) { txn.End() - if out := w.Body.String(); "zap" != out { + if out := w.Body.String(); out != "zap" { t.Error(out) } diff --git a/v3/newrelic/internal_txn.go b/v3/newrelic/internal_txn.go index b98fb45c2..32060485b 100644 --- a/v3/newrelic/internal_txn.go +++ b/v3/newrelic/internal_txn.go @@ -40,6 +40,11 @@ type txn struct { 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 { @@ -114,11 +119,13 @@ func newTxn(app *app, run *appRun, name string, opts ...TraceOption) *thread { 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.TraceIDGenerator = run.Reply.TraceIDGenerator - txn.BetterCAT.SetTraceAndTxnIDs(txn.TraceIDGenerator.GenerateTraceID()) + txn.BetterCAT.SetTraceAndTxnIDs(traceID) txn.BetterCAT.Priority = newPriorityFromRandom(txn.TraceIDGenerator.Float32) txn.ShouldCollectSpanEvents = txn.shouldCollectSpanEvents txn.ShouldCreateSpanGUID = txn.shouldCreateSpanGUID @@ -258,6 +265,11 @@ func (thd *thread) StoreLog(log *logEvent) { 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) } @@ -265,11 +277,11 @@ func (thd *thread) StoreLog(log *logEvent) { } func (txn *txn) freezeName() { - if txn.ignore || ("" != txn.FinalName) { + if txn.ignore || (txn.FinalName != "") { return } txn.FinalName = txn.appRun.createTransactionName(txn.Name, txn.IsWeb) - if "" == txn.FinalName { + if txn.FinalName == "" { txn.ignore = true } } @@ -289,7 +301,6 @@ func (txn *txn) shouldSaveTrace() bool { } func (txn *txn) MergeIntoHarvest(h *harvest) { - var priority priority if txn.BetterCAT.Enabled { priority = txn.BetterCAT.Priority @@ -314,19 +325,30 @@ func (txn *txn) MergeIntoHarvest(h *harvest) { 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) + 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 is not used in error events, remove the reference + // 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) } } @@ -366,7 +388,8 @@ func headersJustWritten(thd *thread, code int, hdr http.Header) { if txn.appRun.responseCodeIsError(code) { e := txnErrorFromResponseCode(time.Now(), code) e.Stack = getStackTrace() - thd.noticeErrorInternal(e, false) + expect := txn.appRun.responseCodeIsExpected(code) + thd.noticeErrorInternal(e, nil, expect) } } @@ -425,7 +448,7 @@ func (thd *thread) End(recovered interface{}) error { if nil != recovered { e := txnErrorFromPanic(time.Now(), recovered) e.Stack = getStackTrace() - thd.noticeErrorInternal(e, false) + thd.noticeErrorInternal(e, nil, false) log.Println(string(debug.Stack())) } @@ -480,8 +503,9 @@ func (thd *thread) End(recovered interface{}) error { if txn.rootSpanErrData != nil { root.AgentAttributes.addString(SpanAttributeErrorClass, txn.rootSpanErrData.Klass) - root.AgentAttributes.addString(SpanAttributeErrorMessage, txn.rootSpanErrData.Msg) + 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 @@ -502,7 +526,7 @@ func (thd *thread) End(recovered interface{}) error { // segments occur. for _, evt := range txn.SpanEvents { evt.TraceID = txn.BetterCAT.TraceID - evt.TransactionID = txn.BetterCAT.TxnID + evt.TransactionID = txn.TxnID evt.Sampled = txn.BetterCAT.Sampled evt.Priority = txn.BetterCAT.Priority } @@ -526,6 +550,17 @@ func (thd *thread) End(recovered interface{}) error { 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() @@ -559,7 +594,7 @@ const ( securityPolicyErrorMsg = "message removed by security policy" ) -func (thd *thread) noticeErrorInternal(err errorData, expect bool) error { +func (thd *thread) noticeErrorInternal(errData errorData, err error, expect bool) error { txn := thd.txn if !txn.Config.ErrorCollector.Enabled { return errorsDisabled @@ -575,19 +610,14 @@ func (thd *thread) noticeErrorInternal(err errorData, expect bool) error { txn.Errors = newTxnErrors(maxTxnErrors) } - if txn.Config.HighSecurity { - err.Msg = highSecurityErrorMsg - } - - if !txn.Reply.SecurityPolicies.AllowRawExceptionMessages.Enabled() { - err.Msg = securityPolicyErrorMsg - } + errData.RawError = err if txn.shouldCollectSpanEvents() { - err.SpanID = txn.CurrentSpanIdentifier(thd.thread) - addErrorAttrs(thd, err) + errData.SpanID = txn.CurrentSpanIdentifier(thd.thread) + addErrorAttrs(thd, errData) } - txn.Errors.Add(err) + + txn.Errors.Add(errData) txn.txnData.txnEvent.HasError = true //mark transaction as having an error return nil } @@ -607,7 +637,7 @@ func addErrorAttrs(t *thread, err errorData) { t.thread.RemoveErrorSpanAttribute(attr) } t.thread.AddAgentSpanAttribute(SpanAttributeErrorClass, err.Klass) - t.thread.AddAgentSpanAttribute(SpanAttributeErrorMessage, err.Msg) + t.thread.AddAgentSpanAttribute(SpanAttributeErrorMessage, scrubbedErrorMessage(err.Msg, t.txn)) } var ( @@ -651,17 +681,17 @@ func errorAttributesMethod(err error) map[string]interface{} { func errDataFromError(input error, expect bool) (data errorData, err error) { cause := errorCause(input) - + validatedErrorMsg := truncateStringMessageIfLong(input.Error()) data = errorData{ When: time.Now(), - Msg: input.Error(), + Msg: validatedErrorMsg, Expect: expect, } - if c := errorClassMethod(input); "" != c { + if c := errorClassMethod(input); c != "" { // If the error implements ErrorClasser, use that. data.Klass = c - } else if c := errorClassMethod(cause); "" != c { + } else if c := errorClassMethod(cause); c != "" { // Otherwise, if the error's cause implements ErrorClasser, use that. data.Klass = c } else { @@ -729,7 +759,7 @@ func (thd *thread) NoticeError(input error, expect bool) error { data.ExtraAttributes = nil } - return thd.noticeErrorInternal(data, expect) + return thd.noticeErrorInternal(data, input, expect) } func (txn *txn) SetName(name string) error { @@ -744,6 +774,12 @@ func (txn *txn) SetName(name string) error { 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() @@ -829,7 +865,7 @@ func (txn *txn) BrowserTimingHeader() (*BrowserTimingHeader, error) { ApplicationID: txn.Reply.AppID, TransactionName: name, QueueTimeMillis: txn.Queuing.Nanoseconds() / (1000 * 1000), - ApplicationTimeMillis: time.Now().Sub(txn.Start).Nanoseconds() / (1000 * 1000), + ApplicationTimeMillis: time.Since(txn.Start).Nanoseconds() / (1000 * 1000), ObfuscatedAttributes: attrs, ErrorBeacon: txn.Reply.ErrorBeacon, Agent: txn.Reply.JSAgentFile, @@ -847,7 +883,6 @@ 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) @@ -893,6 +928,9 @@ func endDatastore(s *DatastoreSegment) error { 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() { @@ -924,7 +962,7 @@ func endDatastore(s *DatastoreSegment) error { } func externalSegmentMethod(s *ExternalSegment) string { - if "" != s.Procedure { + if s.Procedure != "" { return s.Procedure } r := s.Request @@ -933,7 +971,7 @@ func externalSegmentMethod(s *ExternalSegment) string { } if nil != r { - if "" != r.Method { + if r.Method != "" { return r.Method } // Golang's http package states that when a client's Request has @@ -1002,7 +1040,7 @@ func endMessage(s *MessageProducerSegment) error { return errAlreadyEnded } - if "" == s.DestinationType { + if s.DestinationType == "" { s.DestinationType = MessageQueue } @@ -1084,7 +1122,7 @@ func (thd *thread) CreateDistributedTracePayload(hdrs http.Header) { return } - if "" == txn.Reply.AccountID || "" == txn.Reply.TrustedAccountKey { + 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. @@ -1109,7 +1147,7 @@ func (thd *thread) CreateDistributedTracePayload(hdrs http.Header) { p.Priority = txn.BetterCAT.Priority p.Timestamp.Set(txn.Reply.DistributedTraceTimestampGenerator()) p.TrustedAccountKey = txn.Reply.TrustedAccountKey - p.TransactionID = txn.BetterCAT.TxnID // Set the transaction ID to the transaction guid. + 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 @@ -1189,7 +1227,7 @@ func (txn *txn) acceptDistributedTraceHeadersLocked(t TransportType, hdrs http.H return nil } - if "" == txn.Reply.AccountID || "" == txn.Reply.TrustedAccountKey { + 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. @@ -1209,7 +1247,7 @@ func (txn *txn) acceptDistributedTraceHeadersLocked(t TransportType, hdrs http.H // and let's also do our trustedKey check receivedTrustKey := payload.TrustedAccountKey - if "" == receivedTrustKey { + if receivedTrustKey == "" { receivedTrustKey = payload.Account } @@ -1221,7 +1259,7 @@ func (txn *txn) acceptDistributedTraceHeadersLocked(t TransportType, hdrs http.H return errTrustedAccountKey } - if 0 != payload.Priority { + if payload.Priority != 0 { txn.BetterCAT.Priority = payload.Priority } @@ -1259,7 +1297,7 @@ func (thd *thread) AddUserSpanAttribute(key string, val interface{}) error { txn.Lock() defer txn.Unlock() - if outputDests := applyAttributeConfig(thd.Attrs.config, key, destSpan); 0 == outputDests { + if outputDests := applyAttributeConfig(thd.Attrs.config, key, destSpan); outputDests == 0 { return nil } @@ -1334,3 +1372,32 @@ func (txn *txn) IsSampled() bool { 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() any { + txn.Lock() + defer txn.Unlock() + return txn.csecAttributes +} + +func (txn *txn) setCsecAttributes(key, value string) { + txn.Lock() + defer txn.Unlock() + if txn.csecAttributes == nil { + txn.csecAttributes = map[string]any{} + } + txn.csecAttributes[key] = value +} diff --git a/v3/newrelic/internal_txn_test.go b/v3/newrelic/internal_txn_test.go index 09cf7770c..bbe42c9d0 100644 --- a/v3/newrelic/internal_txn_test.go +++ b/v3/newrelic/internal_txn_test.go @@ -567,6 +567,46 @@ func TestNilTransaction(t *testing.T) { } } +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 TestEmptyTransaction(t *testing.T) { txn := &Transaction{} diff --git a/v3/newrelic/limits.go b/v3/newrelic/limits.go index 372ea8c53..7ab3e381c 100644 --- a/v3/newrelic/limits.go +++ b/v3/newrelic/limits.go @@ -36,13 +36,8 @@ const ( maxSyntheticsTraces = 20 maxHarvestErrors = 20 maxHarvestSlowSQLs = 10 - // maxSpanEvents is the maximum number of Span Events that can be captured - // per 60-second harvest cycle - // DEPRECATED: replaced with DistributedTracer.ReservoirLimit configuration value - // This constant is the default we start that value as, but it can be changed at runtime. - // always find the dynamic value, e.g. run.MaxSpanEvents(), instead of this value. - defaultMaxSpanEvents = 2000 + errorEventMessageLengthLimit = 4096 // attributes attributeKeyLengthLimit = 255 attributeValueLengthLimit = 255 diff --git a/v3/newrelic/log_event.go b/v3/newrelic/log_event.go index ee81effff..ff7e46d01 100644 --- a/v3/newrelic/log_event.go +++ b/v3/newrelic/log_event.go @@ -19,19 +19,25 @@ const ( ) type logEvent struct { - priority priority - timestamp int64 - severity string - message string - spanID string - traceID string + 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. + 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. @@ -51,6 +57,14 @@ func (e *logEvent) WriteJSON(buf *bytes.Buffer) { 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('}') } @@ -84,10 +98,11 @@ func (data *LogData) toLogEvent() (logEvent, error) { data.Severity = strings.TrimSpace(data.Severity) event := logEvent{ - priority: newPriority(), - message: data.Message, - severity: data.Severity, - timestamp: data.Timestamp, + priority: newPriority(), + message: data.Message, + severity: data.Severity, + timestamp: data.Timestamp, + attributes: data.Attributes, } return event, nil @@ -171,6 +186,12 @@ func EnrichLog(buf *bytes.Buffer, opts EnricherOption) error { 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 } @@ -193,25 +214,16 @@ func (md *linkingMetadata) appendLinkingMetadata(buf *bytes.Buffer) { addDynamicSpacing(buf) buf.WriteString("NR-LINKING|") - if md.traceID != "" && md.spanID != "" { - 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('|') - } else { - buf.WriteString(md.entityGUID) - buf.WriteByte('|') - buf.WriteString(md.hostname) - buf.WriteByte('|') - buf.WriteString(md.entityName) - buf.WriteByte('|') - } + 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) { diff --git a/v3/newrelic/log_events.go b/v3/newrelic/log_events.go index c02b76058..df3861570 100644 --- a/v3/newrelic/log_events.go +++ b/v3/newrelic/log_events.go @@ -60,7 +60,7 @@ 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 (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] } diff --git a/v3/newrelic/log_events_test.go b/v3/newrelic/log_events_test.go index 2a796d06a..e359ef55a 100644 --- a/v3/newrelic/log_events_test.go +++ b/v3/newrelic/log_events_test.go @@ -36,19 +36,20 @@ func loggingConfigEnabled(limit int) loggingConfig { } } -func sampleLogEvent(priority priority, severity, message string) *logEvent { +func sampleLogEvent(priority priority, severity, message string, attributes map[string]any) *logEvent { return &logEvent{ - priority: priority, - severity: severity, - message: message, - timestamp: 123456, + 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")) - events.Add(sampleLogEvent(0.5, infoLevel, "message2")) + events.Add(sampleLogEvent(0.5, infoLevel, "message1", nil)) + events.Add(sampleLogEvent(0.5, infoLevel, "message2", nil)) json, err := events.CollectorJSON(agentRunID) if nil != err { @@ -70,6 +71,53 @@ func TestBasicLogEvents(t *testing.T) { } } +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(5)) + 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}})) + + 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]"}}]}]` + + if string(json) != expected { + t.Error("actual not equal to expected:\n", string(json), "\n", expected) + } + if events.numSeen != 5 { + t.Error(events.numSeen) + } + if events.NumSaved() != 5 { + t.Error(events.NumSaved()) + } +} + func TestEmptyLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) json, err := events.CollectorJSON(agentRunID) @@ -79,10 +127,10 @@ func TestEmptyLogEvents(t *testing.T) { if nil != json { t.Error(string(json)) } - if 0 != events.numSeen { + if events.numSeen != 0 { t.Error(events.numSeen) } - if 0 != events.NumSaved() { + if events.NumSaved() != 0 { t.Error(events.NumSaved()) } } @@ -91,12 +139,12 @@ func TestEmptyLogEvents(t *testing.T) { func TestSamplingLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - events.Add(sampleLogEvent(0.999999, infoLevel, "a")) - events.Add(sampleLogEvent(0.1, infoLevel, "b")) - events.Add(sampleLogEvent(0.9, infoLevel, "c")) - events.Add(sampleLogEvent(0.2, infoLevel, "d")) - events.Add(sampleLogEvent(0.8, infoLevel, "e")) - events.Add(sampleLogEvent(0.3, infoLevel, "f")) + 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 { @@ -141,14 +189,14 @@ func TestMergeFullLogEvents(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + 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")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + 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) @@ -176,14 +224,14 @@ func TestLogEventMergeFailedSuccess(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + 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")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + 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) @@ -214,14 +262,14 @@ func TestLogEventMergeFailedLimitReached(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + 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")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + 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 @@ -253,7 +301,7 @@ 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))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } // Test that the capacity cannot exceed the max. if 10 != events.capacity() { @@ -292,7 +340,7 @@ 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))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) @@ -322,7 +370,7 @@ 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))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) @@ -356,7 +404,7 @@ func TestLogEventsZeroCapacity(t *testing.T) { 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")) + 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()) } @@ -375,7 +423,7 @@ func TestLogEventCollectionDisabled(t *testing.T) { 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")) + 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()) } @@ -467,6 +515,7 @@ func BenchmarkRecordLoggingMetrics(b *testing.B) { for i := 0; i < internal.MaxLogEvents; i++ { logEvent := logEvent{ + nil, newPriority(), 123456, "INFO", diff --git a/v3/newrelic/metrics.go b/v3/newrelic/metrics.go index 273a5bead..27a4bae91 100644 --- a/v3/newrelic/metrics.go +++ b/v3/newrelic/metrics.go @@ -117,7 +117,7 @@ func (mt *metricTable) mergeFailed(from *metricTable) { } func (mt *metricTable) merge(from *metricTable, newScope string) { - if "" == newScope { + if newScope == "" { for id, m := range from.metrics { mt.mergeMetric(id, *m) } diff --git a/v3/newrelic/secure_agent.go b/v3/newrelic/secure_agent.go new file mode 100644 index 000000000..40334a53d --- /dev/null +++ b/v3/newrelic/secure_agent.go @@ -0,0 +1,155 @@ +package 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 + if app.app.run != nil { + 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 + if app.run != nil { + 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) { + if l := len(b.buf); len(p) <= secureAgent.RequestBodyReadLimit()-l { + b.buf = append(b.buf, p...) + return len(p), nil + } else if l := len(b.buf); secureAgent.RequestBodyReadLimit()-l > 1 { + end := secureAgent.RequestBodyReadLimit() - l + b.buf = append(b.buf, p[:end-1]...) + return end, nil + } else { + b.isDataTruncated = true + return 0, 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 + +} diff --git a/v3/newrelic/segments.go b/v3/newrelic/segments.go index 9d11dfeed..328db4283 100644 --- a/v3/newrelic/segments.go +++ b/v3/newrelic/segments.go @@ -52,6 +52,9 @@ type DatastoreSegment struct { // 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 @@ -67,6 +70,23 @@ type DatastoreSegment struct { // 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 @@ -101,6 +121,10 @@ type ExternalSegment struct { // 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. @@ -149,6 +173,12 @@ 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, @@ -208,6 +238,10 @@ func (s *ExternalSegment) End() { } 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. @@ -252,6 +286,23 @@ 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 @@ -278,7 +329,6 @@ func StartSegment(txn *Transaction, name string) *Segment { // // 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) @@ -287,13 +337,18 @@ func StartExternalSegment(txn *Transaction, request *http.Request) *ExternalSegm 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 diff --git a/v3/newrelic/serverless.go b/v3/newrelic/serverless.go index b2e45c368..add2c26f6 100644 --- a/v3/newrelic/serverless.go +++ b/v3/newrelic/serverless.go @@ -111,6 +111,7 @@ func (sh *serverlessHarvest) Write(arn string, writer io.Writer) { 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 } @@ -156,5 +157,7 @@ func (sh *serverlessHarvest) Write(arn string, writer io.Writer) { 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)) } diff --git a/v3/newrelic/slow_queries_test.go b/v3/newrelic/slow_queries_test.go index 3d831f427..9aa112ad6 100644 --- a/v3/newrelic/slow_queries_test.go +++ b/v3/newrelic/slow_queries_test.go @@ -211,6 +211,7 @@ func TestSlowQueriesBetterCAT(t *testing.T) { FinalName: "WebTransaction/Go/hello", Duration: 3 * time.Second, Attrs: attr, + TxnID: "my-txn-id", BetterCAT: betterCAT{ Enabled: true, TxnID: "txn-id", diff --git a/v3/newrelic/sql_driver.go b/v3/newrelic/sql_driver.go index 803452cc3..c32fa1f08 100644 --- a/v3/newrelic/sql_driver.go +++ b/v3/newrelic/sql_driver.go @@ -1,6 +1,7 @@ // Copyright 2020 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +//go:build go1.10 // +build go1.10 package newrelic @@ -8,6 +9,7 @@ package newrelic import ( "context" "database/sql/driver" + "fmt" "time" ) @@ -42,15 +44,27 @@ func InstrumentSQLConnector(connector driver.Connector, bld SQLDriverSegmentBuil 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; nil != f { + if f := bld.ParseDSN; f != nil { f(&bld.BaseSegment, dsn) } return bld } func (bld SQLDriverSegmentBuilder) useQuery(query string) SQLDriverSegmentBuilder { - if f := bld.ParseQuery; nil != f { + if f := bld.ParseQuery; f != nil { f(&bld.BaseSegment, query) } return bld @@ -111,7 +125,7 @@ func (w *wrapDriver) OpenConnector(name string) (driver.Connector, error) { func (w *wrapConnector) Connect(ctx context.Context) (driver.Conn, error) { original, err := w.original.Connect(ctx) - if nil != err { + if err != nil { return nil, err } return optionalMethodsConn(&wrapConn{ @@ -128,7 +142,7 @@ func (w *wrapConnector) Driver() driver.Driver { } func prepare(original driver.Stmt, err error, bld SQLDriverSegmentBuilder, query string) (driver.Stmt, error) { - if nil != err { + if err != nil { return nil, err } return optionalMethodsStmt(&wrapStmt{ @@ -139,12 +153,18 @@ func prepare(original driver.Stmt, err error, bld SQLDriverSegmentBuilder, query 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) } @@ -163,13 +183,32 @@ func (w *wrapConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.T // Exec implements Execer. func (w *wrapConn) Exec(query string, args []driver.Value) (driver.Result, error) { - return w.original.(driver.Execer).Exec(query, args) + 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) + result, err = w.original.(driver.ExecerContext).ExecContext(ctx, query, args) if err != driver.ErrSkip { seg := w.bld.useQuery(query).startSegmentAt(ctx, startTime) seg.End() @@ -188,13 +227,32 @@ func (w *wrapConn) Ping(ctx context.Context) error { } func (w *wrapConn) Query(query string, args []driver.Value) (driver.Rows, error) { - return w.original.(driver.Queryer).Query(query, args) + 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) + rows, err = w.original.(driver.QueryerContext).QueryContext(ctx, query, args) if err != driver.ErrSkip { seg := w.bld.useQuery(query).startSegmentAt(ctx, startTime) seg.End() @@ -216,11 +274,31 @@ func (w *wrapStmt) NumInput() int { } func (w *wrapStmt) Exec(args []driver.Value) (driver.Result, error) { - return w.original.Exec(args) + 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) { - return w.original.Query(args) + 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. @@ -235,16 +313,34 @@ func (w *wrapStmt) CheckNamedValue(v *driver.NamedValue) error { // 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) + 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) + rows, err = w.original.(driver.StmtQueryContext).QueryContext(ctx, args) segment.End() return rows, err } diff --git a/v3/newrelic/sqlparse/sqlparse.go b/v3/newrelic/sqlparse/sqlparse.go index 3b02dccdb..98afccb3d 100644 --- a/v3/newrelic/sqlparse/sqlparse.go +++ b/v3/newrelic/sqlparse/sqlparse.go @@ -61,6 +61,7 @@ func ParseQuery(segment *newrelic.DatastoreSegment, query string) { 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]) diff --git a/v3/newrelic/stacktrace.go b/v3/newrelic/stacktrace.go index 2891acd67..6358f8ce4 100644 --- a/v3/newrelic/stacktrace.go +++ b/v3/newrelic/stacktrace.go @@ -21,13 +21,13 @@ func getStackTrace() stackTrace { return callers[:written] } -type stacktraceFrame struct { +type StacktraceFrame struct { Name string File string Line int64 } -func (f stacktraceFrame) formattedName() string { +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" @@ -36,7 +36,7 @@ func (f stacktraceFrame) formattedName() string { return path.Base(f.Name) } -func (f stacktraceFrame) isAgent() bool { +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" @@ -44,7 +44,7 @@ func (f stacktraceFrame) isAgent() bool { strings.Contains(f.Name, "github.com/newrelic/go-agent/v3/newrelic.") } -func (f stacktraceFrame) WriteJSON(buf *bytes.Buffer) { +func (f StacktraceFrame) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('{') w := jsonFieldsWriter{buf: buf} if f.Name != "" { @@ -59,7 +59,7 @@ func (f stacktraceFrame) WriteJSON(buf *bytes.Buffer) { buf.WriteByte('}') } -func writeFrames(buf *bytes.Buffer, frames []stacktraceFrame) { +func writeFrames(buf *bytes.Buffer, frames []StacktraceFrame) { // Remove top agent frames. for len(frames) > 0 && frames[0].isAgent() { frames = frames[1:] @@ -80,17 +80,17 @@ func writeFrames(buf *bytes.Buffer, frames []stacktraceFrame) { buf.WriteByte(']') } -func (st stackTrace) frames() []stacktraceFrame { +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) + fs := make([]StacktraceFrame, 0, maxStackTraceFrames) var frame runtime.Frame more := true for more { frame, more = frames.Next() - fs = append(fs, stacktraceFrame{ + fs = append(fs, StacktraceFrame{ Name: frame.Function, File: frame.File, Line: int64(frame.Line), diff --git a/v3/newrelic/stacktrace_test.go b/v3/newrelic/stacktrace_test.go index b286489cf..6e9c6f74d 100644 --- a/v3/newrelic/stacktrace_test.go +++ b/v3/newrelic/stacktrace_test.go @@ -37,7 +37,7 @@ func TestLongStackTraceLimitsFrames(t *testing.T) { } func TestManyStackTraceFramesLimitsOutput(t *testing.T) { - frames := make([]stacktraceFrame, maxStackTraceFrames+20) + frames := make([]StacktraceFrame, maxStackTraceFrames+20) expect := `[ {},{},{},{},{},{},{},{},{},{}, {},{},{},{},{},{},{},{},{},{}, @@ -60,7 +60,7 @@ func TestManyStackTraceFramesLimitsOutput(t *testing.T) { func TestStacktraceFrames(t *testing.T) { // This stacktrace taken from Go 1.13 - inputFrames := []stacktraceFrame{ + 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", diff --git a/v3/newrelic/trace_observer.go b/v3/newrelic/trace_observer.go index 784dfa921..4274c05ed 100644 --- a/v3/newrelic/trace_observer.go +++ b/v3/newrelic/trace_observer.go @@ -1,7 +1,9 @@ // 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 GRPC/ProtoBuf libraries only support Go version 1.9 and up. package newrelic @@ -184,10 +186,11 @@ func newDialOptions(cfg observerConfig) []grpc.DialOption { 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": err.Error(), + "err": errMsg, }) return } diff --git a/v3/newrelic/trace_observer_impl_test.go b/v3/newrelic/trace_observer_impl_test.go index c6dd45c99..17b66035b 100644 --- a/v3/newrelic/trace_observer_impl_test.go +++ b/v3/newrelic/trace_observer_impl_test.go @@ -1,7 +1,9 @@ // 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 @@ -215,6 +217,20 @@ func (s *expectServer) DidSpansArrive(t *testing.T, expected int, timeout time.D } } +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 { diff --git a/v3/newrelic/trace_observer_test.go b/v3/newrelic/trace_observer_test.go index f746416c8..ee1f9f2c1 100644 --- a/v3/newrelic/trace_observer_test.go +++ b/v3/newrelic/trace_observer_test.go @@ -902,8 +902,8 @@ func TestTrObsOKSendBackoffNo(t *testing.T) { } // If the default backoff of 15 seconds is used, the second span will not // be received in time. - if !s.DidSpansArrive(t, 2, 8*time.Second) { - t.Error("server did not receive 2 spans") + if !s.DidSpansArriveNoTimeout(t, 1) { + t.Error("server did not receive a span") } } @@ -944,8 +944,8 @@ func TestTrObsOKReceiveBackoffNo(t *testing.T) { } // If the default backoff of 15 seconds is used, the second span will not // be received in time. - if !s.DidSpansArrive(t, 2, time.Second) { - t.Error("server did not receive 2 spans") + if !s.DidSpansArriveNoTimeout(t, 1) { + t.Error("server did not receive a span") } } diff --git a/v3/newrelic/tracing.go b/v3/newrelic/tracing.go index d6a966401..c8fac7ef6 100644 --- a/v3/newrelic/tracing.go +++ b/v3/newrelic/tracing.go @@ -35,6 +35,8 @@ type txnEvent struct { externalDuration time.Duration datastoreCallCount uint64 datastoreDuration time.Duration + errGroupCallback ErrorGroupCallback + TxnID string } // betterCAT stores the transaction's priority and all fields related @@ -61,6 +63,16 @@ func (bc *betterCAT) SetTraceAndTxnIDs(traceID string) { } } +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 @@ -247,7 +259,7 @@ func (m *spanAttributeMap) addAgentAttrs(attrs agentAttributes) { } } -func addAttr(m *spanAttributeMap, key string, val interface{}) { +func addAttr(m *spanAttributeMap, key string, val any) { switch v := val.(type) { case string: m.addString(key, v) @@ -356,7 +368,7 @@ func (thread *tracingThread) AddAgentSpanAttribute(key string, val string) { } // AddUserSpanAttribute allows custom attributes to be added to spans. -func (thread *tracingThread) AddUserSpanAttribute(key string, val interface{}) { +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{ @@ -413,7 +425,7 @@ func (t *txnData) CurrentSpanIdentifier(thread *tracingThread) string { func (t *txnData) saveSpanEvent(e *spanEvent) { e.AgentAttributes = t.Attrs.filterSpanAttributes(e.AgentAttributes, destSpan) - if len(t.SpanEvents) < defaultMaxSpanEvents { + if len(t.SpanEvents) < internal.MaxSpanEvents { t.SpanEvents = append(t.SpanEvents, e) } } @@ -564,7 +576,7 @@ func endExternalSegment(p endExternalParams) error { 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]interface{}{ + p.Logger.Debug("failure to parse cross application response header", map[string]any{ "err": err.Error(), "header": hdr, }) @@ -698,7 +710,7 @@ type endDatastoreParams struct { Collection string Operation string ParameterizedQuery string - QueryParameters map[string]interface{} + QueryParameters map[string]any Host string PortPathOrID string Database string diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 31f22b39e..8a17c985b 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -6,6 +6,7 @@ package newrelic import ( "encoding/json" "fmt" + "io" "net/http" "net/url" "strings" @@ -19,7 +20,7 @@ import ( // All methods on Transaction are nil safe. Therefore, a nil Transaction // pointer can be safely used as a mock. type Transaction struct { - Private interface{} + Private any thread *thread } @@ -27,18 +28,22 @@ type Transaction struct { // other Transaction methods have no effect. All segments and // instrumentation must be completed before End is called. func (txn *Transaction) End() { - if nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } - var r interface{} + 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.thread.logAPIError(txn.thread.End(r), "end transaction", nil) } @@ -58,10 +63,7 @@ func (txn *Transaction) SetOption(options ...TraceOption) { // Ignore prevents this transaction's data from being recorded. func (txn *Transaction) Ignore() { - if nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } txn.thread.logAPIError(txn.thread.Ignore(), "ignore transaction", nil) @@ -70,15 +72,25 @@ func (txn *Transaction) Ignore() { // 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 nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { 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 txn == nil || txn.thread == nil { + 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. @@ -99,16 +111,13 @@ func (txn *Transaction) SetName(name string) { // ErrorClass() string // // // ErrorAttributes sets the errors attributes -// ErrorAttributes() map[string]interface{} +// 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 nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } txn.thread.logAPIError(txn.thread.NoticeError(err, false), "notice error", nil) @@ -136,16 +145,13 @@ func (txn *Transaction) NoticeError(err error) { // ErrorClass() string // // // ErrorAttributes sets the errors attributes -// ErrorAttributes() map[string]interface{} +// 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 nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } txn.thread.logAPIError(txn.thread.NoticeError(err, true), "notice error", nil) @@ -159,14 +165,22 @@ func (txn *Transaction) NoticeExpectedError(err error) { // // 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 interface{}) { - if nil == txn { +func (txn *Transaction) AddAttribute(key string, value any) { + if txn == nil || txn.thread == nil { return } - if nil == txn.thread { + 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 txn == nil || txn.thread == nil { return } - txn.thread.logAPIError(txn.thread.AddAttribute(key, value), "add attribute", nil) + + txn.thread.logAPIError(txn.thread.AddUserID(userID), "set user ID", nil) } // RecordLog records the data from a single log line. @@ -180,7 +194,7 @@ func (txn *Transaction) AddAttribute(key string, value interface{}) { func (txn *Transaction) RecordLog(log LogData) { event, err := log.toLogEvent() if err != nil { - txn.Application().app.Error("unable to record log", map[string]interface{}{ + txn.Application().app.Error("unable to record log", map[string]any{ "reason": err.Error(), }) return @@ -198,16 +212,20 @@ func (txn *Transaction) RecordLog(log LogData) { // present, the agent will look for distributed tracing headers using // Transaction.AcceptDistributedTraceHeaders. func (txn *Transaction) SetWebRequestHTTP(r *http.Request) { - if nil == r { + if r == nil { txn.SetWebRequest(WebRequest{}) return } wr := WebRequest{ - Header: r.Header, - URL: r.URL, - Method: r.Method, - Transport: transport(r), - Host: r.Host, + 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) } @@ -222,17 +240,36 @@ func transport(r *http.Request) TransportType { 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 nil == txn { + if txn == nil || txn.thread == nil { return } - if nil == txn.thread { - return + if IsSecurityAgentPresent() { + secureAgent.SendEvent("INBOUND", r, txn.GetCsecAttributes()) } txn.thread.logAPIError(txn.thread.SetWebRequest(r), "set web request", nil) } @@ -252,10 +289,7 @@ func (txn *Transaction) SetWebRequest(r WebRequest) { // 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 nil == txn { - return w - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return w } return txn.thread.SetWebResponse(w) @@ -270,10 +304,7 @@ func (txn *Transaction) StartSegmentNow() SegmentStartTime { } func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { - if nil == txn { - return SegmentStartTime{} - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return SegmentStartTime{} } return txn.thread.startSegmentAt(at) @@ -293,6 +324,10 @@ func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { // // ... code you want to time here ... // segment.End() func (txn *Transaction) StartSegment(name string) *Segment { + if IsSecurityAgentPresent() && txn != nil && txn.thread != nil && 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, @@ -311,10 +346,7 @@ func (txn *Transaction) StartSegment(name string) *Segment { // 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 nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } txn.thread.CreateDistributedTracePayload(hdrs) @@ -335,10 +367,7 @@ func (txn *Transaction) InsertDistributedTraceHeaders(hdrs http.Header) { // 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 nil == txn { - return - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return } txn.thread.logAPIError(txn.thread.AcceptDistributedTraceHeaders(t, hdrs), "accept trace payload", nil) @@ -396,7 +425,7 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j // // (Note that the HTTP headers are capitalized.) func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err error) { - var raw interface{} + var raw any hdrs = http.Header{} if jsondata == "" { return @@ -407,12 +436,12 @@ func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err err } switch d := raw.(type) { - case map[string]interface{}: + case map[string]any: for k, v := range d { switch hval := v.(type) { case string: hdrs.Set(k, hval) - case []interface{}: + case []any: for _, subval := range hval { switch sval := subval.(type) { case string: @@ -436,10 +465,7 @@ func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err err // Application returns the Application which started the transaction. func (txn *Transaction) Application() *Application { - if nil == txn { - return nil - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return nil } return txn.thread.Application() @@ -458,10 +484,7 @@ func (txn *Transaction) Application() *Application { // 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 nil == txn { - return nil - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return nil } b, err := txn.thread.BrowserTimingHeader() @@ -483,22 +506,20 @@ func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader { // Note that any segments that end after the transaction ends will not // be reported. func (txn *Transaction) NewGoroutine() *Transaction { - if nil == txn { + if txn == nil || txn.thread == nil { return nil } - if nil == txn.thread { - return nil + newTxn := txn.thread.NewGoroutine() + if IsSecurityAgentPresent() && newTxn.thread != nil { + newTxn.thread.setCsecData() } - return txn.thread.NewGoroutine() + return newTxn } // GetTraceMetadata returns distributed tracing identifiers. Empty // string identifiers are returned if the transaction has finished. func (txn *Transaction) GetTraceMetadata() TraceMetadata { - if nil == txn { - return TraceMetadata{} - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return TraceMetadata{} } return txn.thread.GetTraceMetadata() @@ -507,10 +528,7 @@ func (txn *Transaction) GetTraceMetadata() TraceMetadata { // GetLinkingMetadata returns the fields needed to link data to a trace or // entity. func (txn *Transaction) GetLinkingMetadata() LinkingMetadata { - if nil == txn { - return LinkingMetadata{} - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return LinkingMetadata{} } return txn.thread.GetLinkingMetadata() @@ -521,15 +539,27 @@ func (txn *Transaction) GetLinkingMetadata() LinkingMetadata { // must be enabled for transactions to be sampled. False is returned if // the Transaction has finished. func (txn *Transaction) IsSampled() bool { - if nil == txn { - return false - } - if nil == txn.thread { + if txn == nil || txn.thread == nil { return false } return txn.thread.IsSampled() } +func (txn *Transaction) GetCsecAttributes() any { + if txn == nil || txn.thread == nil { + return nil + } + return txn.thread.getCsecAttributes() +} + +func (txn *Transaction) SetCsecAttributes(key, value string) { + if txn == nil || txn.thread == nil { + return + } + txn.thread.setCsecAttributes(key, value) + +} + const ( // DistributedTraceNewRelicHeader is the header used by New Relic agents // for automatic trace payload instrumentation. @@ -586,6 +616,59 @@ type WebRequest struct { // 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 diff --git a/v3/newrelic/txn_cross_process.go b/v3/newrelic/txn_cross_process.go index ad14237ef..f6ed3eed4 100644 --- a/v3/newrelic/txn_cross_process.go +++ b/v3/newrelic/txn_cross_process.go @@ -53,19 +53,26 @@ type txnCrossProcess struct { 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 + ID string + TxnData string + Synthetics string + SyntheticsInfo string } // Init initialises a txnCrossProcess based on the given application connect @@ -88,6 +95,7 @@ func (txp *txnCrossProcess) CreateCrossProcessMetadata(txnName, appName string) // outbound request headers. if txp.IsSynthetics() { metadata.Synthetics = txp.SyntheticsHeader + metadata.SyntheticsInfo = txp.SyntheticsInfoHeader } if txp.Enabled { @@ -142,7 +150,7 @@ func (txp *txnCrossProcess) IsSynthetics() bool { // 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 0 != (txp.Type&txnCrossProcessSynthetics) && nil != txp.Synthetics + return (txp.Type&txnCrossProcessSynthetics) != 0 && txp.Synthetics != nil } // ParseAppData decodes the given appData value. @@ -247,6 +255,11 @@ func (txp *txnCrossProcess) handleInboundRequestHeaders(metadata crossProcessMet 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 @@ -338,6 +351,36 @@ func (txp *txnCrossProcess) handleInboundRequestSynthetics(raw []byte) error { 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) } diff --git a/v3/newrelic/txn_cross_process_test.go b/v3/newrelic/txn_cross_process_test.go index de07d4890..8e8807d3c 100644 --- a/v3/newrelic/txn_cross_process_test.go +++ b/v3/newrelic/txn_cross_process_test.go @@ -85,6 +85,18 @@ func (req *request) withSynthetics(account int, encodingKey string) *request { } 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 } @@ -168,14 +180,16 @@ func TestTxnCrossProcessInit(t *testing.T) { 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}) + 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) @@ -241,7 +255,8 @@ func TestTxnCrossProcessCreateCrossProcessMetadata(t *testing.T) { appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ - Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), + Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), + SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, { @@ -253,7 +268,8 @@ func TestTxnCrossProcessCreateCrossProcessMetadata(t *testing.T) { appName: "app", expectedError: false, expectedMetadata: crossProcessMetadata{ - Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), + Synthetics: mustObfuscate(`[1,1,"resource","job","monitor"]`, "foo"), + SyntheticsInfo: mustObfuscate(`{"version":1,"type":"scheduled","initiator":"cli"}`, "foo"), }, }, { @@ -278,9 +294,10 @@ func TestTxnCrossProcessCreateCrossProcessMetadata(t *testing.T) { 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"), + 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"), }, }, { @@ -305,9 +322,10 @@ func TestTxnCrossProcessCreateCrossProcessMetadata(t *testing.T) { 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"), + 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"), }, }, } { diff --git a/v3/newrelic/txn_events.go b/v3/newrelic/txn_events.go index 61bd9aff4..2828c863b 100644 --- a/v3/newrelic/txn_events.go +++ b/v3/newrelic/txn_events.go @@ -5,6 +5,7 @@ package newrelic import ( "bytes" + "fmt" "sort" "strings" "time" @@ -113,6 +114,15 @@ func sharedTransactionIntrinsics(e *txnEvent, w *jsonFieldsWriter) { 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) + } + } + } } } diff --git a/v3/newrelic/txn_trace.go b/v3/newrelic/txn_trace.go index 2e925c5e4..f1d0c99ce 100644 --- a/v3/newrelic/txn_trace.go +++ b/v3/newrelic/txn_trace.go @@ -294,6 +294,9 @@ func (trace *harvestTrace) writeJSON(buf *bytes.Buffer) { 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(`""`) } diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index afd00ee60..ebc211672 100644 --- a/v3/newrelic/version.go +++ b/v3/newrelic/version.go @@ -11,7 +11,7 @@ import ( const ( // Version is the full string version of this Go Agent. - Version = "3.20.2" + Version = "3.33.1" ) var ( diff --git a/version.go b/version.go deleted file mode 100644 index 15f922114..000000000 --- a/version.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package newrelic - -import ( - "runtime" - - "github.com/newrelic/go-agent/internal" -) - -const ( - major = "2" - minor = "16" - patch = "4" - - // Version is the full string version of this Go Agent. - Version = major + "." + minor + "." + patch -) - -func init() { - internal.TrackUsage("Go", "Version", Version) - internal.TrackUsage("Go", "Runtime", "Version", internal.MinorVersion(runtime.Version())) -}