diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cb892f3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# See: https://help.github.com/articles/about-codeowners/ + +# These owners will be the default owners for everything in the repo. +* @NipunaMadhushan @keizer619 diff --git a/issue_template.md b/.github/issue_template.md similarity index 100% rename from issue_template.md rename to .github/issue_template.md diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml new file mode 100644 index 0000000..13d24d3 --- /dev/null +++ b/.github/workflows/build-main.yml @@ -0,0 +1,71 @@ +name: Build + +on: + workflow_dispatch: + inputs: + ballerina_version: + description: 'Ballerina version' + required: true + default: '2201.8.3' + push: + branches: + - main + +env: + BALLERINA_DISTRIBUTION_VERSION: 2201.8.3 # Update this with the latest Ballerina version + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository_owner == 'ballerina-platform' + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Set up Ballerina + if: github.event_name == 'workflow_dispatch' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ github.event.inputs.ballerina_version }} + - name: Set up Ballerina + if: github.event_name == 'push' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ env.BALLERINA_DISTRIBUTION_VERSION }} + - name: Change to Timestamped Version + run: | + startTime=$(TZ="Asia/Kolkata" date +'%Y%m%d-%H%M00') + latestCommit=$(git log -n 1 --pretty=format:"%h") + VERSION=$((grep -w 'version' | cut -d= -f2) < gradle.properties | rev | cut --complement -d- -f1 | rev) + updatedVersion=$VERSION-$startTime-$latestCommit + echo $updatedVersion + echo TIMESTAMPED_VERSION=$updatedVersion >> $GITHUB_ENV + sed -i "s/version=\(.*\)/version=$updatedVersion/g" gradle.properties + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Set Docker Host env variable + run: echo "DOCKER_HOST=$(docker context ls --format '{{print .DockerEndpoint}}' | tr -d '\n')" >> $GITHUB_ENV + - name: Build with Gradle + env: + DOCKER_HOST: unix:///var/run/docker.sock + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + packagePublishRepo: ${{ github.repository }} + run: | + ./gradlew clean build publish --stacktrace --scan --console=plain --no-daemon + ./gradlew codeCoverageReport --console=plain --no-daemon + - name: Upload Coverage Report + uses: codecov/codecov-action@v1 + with: + fail_ci_if_error: true + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: distribution + path: | + ballerina/build/distributions/newrelic-extension-ballerina-*.zip + if-no-files-found: error diff --git a/.github/workflows/central-publish.yml b/.github/workflows/central-publish.yml new file mode 100644 index 0000000..479234c --- /dev/null +++ b/.github/workflows/central-publish.yml @@ -0,0 +1,82 @@ +name: Publish to the Ballerina central + +on: + workflow_dispatch: + inputs: + ballerina_version: + description: 'Ballerina version' + required: true + default: '2201.8.3' + environment: + type: choice + description: Select environment + required: true + options: + - CENTRAL + - DEV CENTRAL + - STAGE CENTRAL + +jobs: + publish-release: + runs-on: ubuntu-latest + if: github.repository_owner == 'ballerina-platform' + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Set up Ballerina + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ github.event.inputs.ballerina_version }} + - name: Build with Gradle + env: + packageUser: ${{ github.actor }} + packagePAT: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew build -x check -x test + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'rootfs' + scan-ref: '/github/workspace/ballerina/lib' + format: 'table' + timeout: '10m0s' + exit-code: '1' + + - name: Ballerina Central Push + if: ${{ github.event.inputs.environment == 'CENTRAL' }} + env: + BALLERINA_DEV_CENTRAL: false + BALLERINA_STAGE_CENTRAL: false + BALLERINA_CENTRAL_ACCESS_TOKEN: ${{ secrets.BALLERINA_CENTRAL_ACCESS_TOKEN }} + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + ./gradlew clean build -PpublishToCentral=true + - name: Ballerina Central Dev Push + if: ${{ github.event.inputs.environment == 'DEV CENTRAL' }} + env: + BALLERINA_DEV_CENTRAL: true + BALLERINA_STAGE_CENTRAL: false + BALLERINA_CENTRAL_ACCESS_TOKEN: ${{ secrets.BALLERINA_CENTRAL_DEV_ACCESS_TOKEN }} + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + sed -i 's/version=\(.*\)-SNAPSHOT/version=\1/g' gradle.properties + ./gradlew clean build -PpublishToCentral=true + - name: Ballerina Central Stage Push + if: ${{ github.event.inputs.environment == 'STAGE CENTRAL' }} + env: + BALLERINA_DEV_CENTRAL: false + BALLERINA_STAGE_CENTRAL: true + BALLERINA_CENTRAL_ACCESS_TOKEN: ${{ secrets.BALLERINA_CENTRAL_STAGE_ACCESS_TOKEN }} + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + sed -i 's/version=\(.*\)-SNAPSHOT/version=\1/g' gradle.properties + ./gradlew clean build -PpublishToCentral=true diff --git a/.github/workflows/daily-build.yml b/.github/workflows/daily-build.yml new file mode 100644 index 0000000..f54d7ff --- /dev/null +++ b/.github/workflows/daily-build.yml @@ -0,0 +1,64 @@ +name: Daily build + +on: + schedule: + - cron: '30 2 * * *' + workflow_dispatch: + inputs: + ballerina_version: + description: 'Ballerina version' + required: true + default: '2201.8.3' + +env: + BALLERINA_DISTRIBUTION_VERSION: 2201.8.3 # Update this with the latest Ballerina version + +jobs: + build: + if: github.repository_owner == 'ballerina-platform' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Set up Ballerina + if: github.event_name == 'workflow_dispatch' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ github.event.inputs.ballerina_version }} + - name: Set up Ballerina + if: github.event_name == 'schedule' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ env.BALLERINA_DISTRIBUTION_VERSION }} + - name: Build with Gradle + env: + packageUser: ${{ github.actor }} + packagePAT: ${{ secrets.GITHUB_TOKEN }} + run: | + ./gradlew clean build + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'rootfs' + scan-ref: '/github/workspace/ballerina/lib' + format: 'table' + timeout: '10m0s' + exit-code: '1' + - name: Notify failure + if: ${{ failure() }} + run: | + curl -X POST \ + 'https://api.github.com/repos/ballerina-platform/ballerina-release/dispatches' \ + -H 'Accept: application/vnd.github.v3+json' \ + -H 'Authorization: Bearer ${{ secrets.BALLERINA_BOT_TOKEN }}' \ + --data "{ + \"event_type\": \"notify-build-failure\", + \"client_payload\": { + \"repoName\": \"module-ballerinax-newrelic\" + } + }" diff --git a/.github/workflows/graalvm-check.yml b/.github/workflows/graalvm-check.yml new file mode 100644 index 0000000..b80a806 --- /dev/null +++ b/.github/workflows/graalvm-check.yml @@ -0,0 +1,62 @@ +name: GraalVM Check + +on: + schedule: + - cron: '30 18 * * *' + workflow_dispatch: + inputs: + ballerina_version: + description: 'Ballerina version' + required: true + default: '2201.8.3' + +env: + BALLERINA_DISTRIBUTION_VERSION: 2201.8.3 # Update this with the latest Ballerina version + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + version: 'latest' + java-version: '17.0.7' + components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Check GraalVM installation + run: | + echo "GRAALVM_HOME: ${{ env.GRAALVM_HOME }}" + echo "JAVA_HOME: ${{ env.JAVA_HOME }}" + native-image --version + - name: Set up Ballerina + if: github.event_name == 'workflow_dispatch' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ github.event.inputs.ballerina_version }} + - name: Set up Ballerina + if: github.event_name == 'schedule' + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ env.BALLERINA_DISTRIBUTION_VERSION }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + env: + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + JAVA_OPTS: -DBALLERINA_DEV_COMPILE_BALLERINA_ORG=true + run: | + ./gradlew build + - name: Update dependency versions + run: ./gradlew :newrelic-extension-ballerina:updateTomlVerions + env: + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + JAVA_OPTS: -DBALLERINA_DEV_COMPILE_BALLERINA_ORG=true + - name: Run Ballerina build using the native executable + run: bal build --native ./ballerina + - name: Run Ballerina tests using the native executable + run: bal test --native ./ballerina diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..32920cf --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,83 @@ +name: Publish Release + +on: + workflow_dispatch: + inputs: + distribution_version: + description: 'Ballerina distribution version (e.g.; 2201.8.0)' + required: true + default: '2201.8.0' + +jobs: + publish-release: + runs-on: ubuntu-latest + if: github.repository_owner == 'ballerina-platform' + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Set up Ballerina + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ github.event.inputs.ballerina_version }} + - name: Set version env variable + run: echo "VERSION=$((grep -w 'version' | cut -d= -f2) < gradle.properties | rev | cut --complement -d- -f1 | rev)" >> $GITHUB_ENV + - name: Pre release depenency version update + env: + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + echo "Version: ${VERSION}" + git config user.name ${{ secrets.BALLERINA_BOT_USERNAME }} + git config user.email ${{ secrets.BALLERINA_BOT_EMAIL }} + git checkout -b release-${VERSION} + git add gradle.properties + git commit -m "Move dependencies to stable version" || echo "No changes to commit" + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Set Docker Host env variable + run: echo "DOCKER_HOST=$(docker context ls --format '{{print .DockerEndpoint}}' | tr -d '\n')" >> $GITHUB_ENV + - name: Build with Gradle + env: + packageUser: ${{ github.actor }} + packagePAT: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew build -x check -x test + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'rootfs' + scan-ref: '/github/workspace/ballerina/lib' + format: 'table' + timeout: '10m0s' + exit-code: '1' + - name: Publish artifact + env: + DOCKER_HOST: unix:///var/run/docker.sock + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + BALLERINA_CENTRAL_ACCESS_TOKEN: ${{ secrets.BALLERINA_CENTRAL_ACCESS_TOKEN }} + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + packagePublishRepo: ${{ github.repository }} + run: | + git stash + ./gradlew release -Prelease.useAutomaticVersion=true + ./gradlew -Pversion=${VERSION} publish -x test + - name: Create Github release from the release tag + run: | + curl --request POST 'https://api.github.com/repos/${{ github.repository }}/releases' \ + --header 'Accept: application/vnd.github.v3+json' \ + --header 'Authorization: Bearer ${{ secrets.BALLERINA_BOT_TOKEN }}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "tag_name": "v'"$VERSION"'", + "name": "module-ballerinax-newrelic-v'"$VERSION"'" + }' + - name: Post release PR + env: + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + curl -fsSL https://github.com/github/hub/raw/master/script/get | bash -s 2.14.1 + bin/hub pull-request --base main -m "[Automated] Sync master after "$VERSION" release" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..f7dcf98 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,37 @@ +name: Validate Pull Request + +on: pull_request + +env: + BALLERINA_DISTRIBUTION_VERSION: 2201.8.3 # Update this with the latest Ballerina version + +jobs: + ubuntu-build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: adopt + java-version: 17.0.7 + - name: Set up Ballerina + uses: ballerina-platform/setup-ballerina@v1.1.0 + with: + version: ${{ env.BALLERINA_DISTRIBUTION_VERSION }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Set Docker Host env variable + run: echo "DOCKER_HOST=$(docker context ls --format '{{print .DockerEndpoint}}' | tr -d '\n')" >> $GITHUB_ENV + - name: Build with Gradle + env: + packageUser: ${{ github.actor }} + packagePAT: ${{ secrets.GITHUB_TOKEN }} + run: | + ./gradlew clean build --stacktrace --scan --console=plain --no-daemon + ./gradlew codeCoverageReport --console=plain --no-daemon + - name: Upload Coverage Report + uses: codecov/codecov-action@v1 + with: + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 524f096..547f3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,34 @@ *.zip *.tar.gz *.rar +*.deb +*.pkg # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# IDEs +.classpath +.project +.settings +.vscode +*.iml +*.iws +*.ipr +.idea + +# Gradle +.gradle +**/build/ +!src/**/build/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache +.DS_Store + +# Ballerina +target/ +.devcontainer.json +lib/ +bin/ diff --git a/README.md b/README.md index c919ea3..c45ce3a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,127 @@ -# module-ballerinax-newrelic -Ballerina New Relic Observability Extension Module +# Ballerina New Relic Observability Extension + +[![Build](https://github.com/ballerina-platform/module-ballerinax-newrelic/workflows/Daily%20Build/badge.svg)](https://github.com/ballerina-platform/module-ballerinax-newrelic/actions?query=workflow%3A"Daily+Build") +[![GitHub Last Commit](https://img.shields.io/github/last-commit/ballerina-platform/module-ballerinax-newrelic.svg)](https://github.com/ballerina-platform/module-ballerinax-newrelic/commits/master) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![codecov](https://codecov.io/gh/ballerina-platform/module-ballerinax-newrelic/branch/main/graph/badge.svg?token=5GCQ36HBEB)](https://codecov.io/gh/ballerina-platform/module-ballerinax-newrelic) + +## Building from the Source + +### Setting Up the Prerequisites + +1. Download and install Java SE Development Kit (JDK) version 17 (from one of the following locations). + + * [Oracle](https://www.oracle.com/java/technologies/downloads/) + + * [OpenJDK](https://adoptopenjdk.net/) + + > **Note:** Set the JAVA_HOME environment variable to the path name of the directory into which you installed JDK. + +### Building the Source + +Execute the commands below to build from source. + +1. To build the library: + + ./gradlew clean build + +2. To run the integration tests: + + ./gradlew clean test + +## Configure Ballerina Project with New Relic Observability Extension + +### Prerequisites + +1. Sign Up and Generate an API Key in [New Relic](https://newrelic.com/) + * To configure the API key in Newrelic: + > Go to Profile -> API keys -> Insights Insert key -> Insert keys to create an account in New Relic. + +### Configure Ballerina Project + +To package the New Relic extension into the Jar, follow the following steps. +1. Add the following import to your program. +```ballerina +import ballerinax/newrelic as _; +``` + +2. Add the following to the `Ballerina.toml` when building your program. +```toml +[package] +org = "my_org" +name = "my_package" +version = "1.0.0" + +[build-options] +observabilityIncluded=true +``` + +3. To enable the extension and publish traces and metrics to New Relic, add the following to the `Config.toml` when running your program. +```toml +[ballerina.observe] +tracingEnabled=true +tracingProvider="newrelic" +metricsEnabled=true +metricsReporter="newrelic" + +[ballerinax.newrelic] +apiKey="" # Mandatory Configuration. +tracingSamplerType="const" # Optional Configuration. Default value is 'const' +tracingSamplerParam=1 # Optional Configuration. Default value is 1 +tracingReporterFlushInterval=15000 # Optional Configuration. Default value is 15000 milliseconds +tracingReporterBufferSize=10000 # Optional Configuration. Default value is 10000 +metricReporterFlushInterval=15000 # Optional Configuration. Default value is 15000 milliseconds +metricReporterClientTimeout=10000 # Optional Configuration. Default value is 10000 milliseconds +``` + +### Observe Metrics in New Relic + +Instead of using prometheus as an intermediate metric reporter that remote writes the metrics to New Relic, +Ballerina New Relic Observability Extension directly publishes metrics to New Relic on the following metric API `https://metric-api.newrelic.com/metric/v1`. + +Instrumentation of metrics is done using the [com.newrelic.telemetry](https://github.com/newrelic/newrelic-telemetry-sdk-java). + +#### Available Metrics + +The exporter provides the following metrics. + +|Metric Name|Description| +|---|---| +|response_time_seconds_value|Response time of a HTTP request in seconds| +|response_time_seconds_max|Maximum response time of a HTTP request| +|response_time_seconds_min|Minimum response time of a HTTP request| +|response_time_seconds_mean|Average response time of a HTTP request| +|response_time_seconds_stdDev|Standard deviation of response time of a HTTP request| +|response_time_seconds|Summary of HTTP request-response times across various time frames and quantiles| +|response_time_nanoseconds_total_value|Response time of a HTTP request in nano seconds| +|requests_total_value|Total number of requests| +|response_errors_total_value|Total number of response errors| +|inprogress_requests_value|Total number of inprogress requests| +|kafka_publishers_value|Number of publishers in kafka| +|kafka_consumers_value|Number of consumers in kafka| +|kafka_errors_value|Number of errors happened while publishing in kafka| + +### Observe Traces in New Relic + +Ballerina New Relic Observability Extension directly publishes traces to New Relic on the following trace API `https://otlp.nr-data.net:4317`. +Traces are published to New Relic on OpenTelemetry format. + +Instrumentation of traces is done using the [io.opentelemetry](https://github.com/open-telemetry/opentelemetry-java) and `GRPC` protocol is used send traces. + + +## Contributing to Ballerina + +As an open source project, Ballerina welcomes contributions from the community. + +For more information, go to the [contribution guidelines](https://github.com/ballerina-platform/ballerina-lang/blob/master/CONTRIBUTING.md). + +## Code of Conduct + +All contributors are encouraged to read the [Ballerina Code of Conduct](https://ballerina.io/code-of-conduct). + +## Useful Links + +* Discuss about code changes of the Ballerina project in [ballerina-dev@googlegroups.com](mailto:ballerina-dev@googlegroups.com). +* Chat live with us via our [Discord server](https://discord.gg/ballerinalang). +* Post all technical questions on Stack Overflow with the [#ballerina](https://stackoverflow.com/questions/tagged/ballerina) tag. +* View the [Ballerina performance test results](https://github.com/ballerina-platform/ballerina-lang/blob/master/performance/benchmarks/summary.md). diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml new file mode 100644 index 0000000..fa15787 --- /dev/null +++ b/ballerina/Ballerina.toml @@ -0,0 +1,181 @@ +# Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +# +# WSO2 LLC. licenses this file to you 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] +org = "ballerinax" +name = "newrelic" +version = "@toml.version@" +distribution = "2201.8.2" + +[platform.java17] +graalvmCompatible = true + +[[platform.java17.dependency]] +path = "../native/build/libs/newrelic-extension-native-@project.version@.jar" +groupId = "ballerina" +artifactId = "newrelic-extension-native" +version = "@project.version@" + +[[platform.java17.dependency]] +path = "./lib/telemetry-all-@newrelic.telemetry.version@.jar" +groupId = "com.newrelic.telemetry" +artifactId = "telemetry-all" +version = "@newrelic.telemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/telemetry-http-okhttp-@newrelic.telemetry.version@.jar" +groupId = "com.newrelic.telemetry" +artifactId = "telemetry-http-okhttp" +version = "@newrelic.telemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/okio-jvm-@okio.version@.jar" +groupId = "com.squareup.okio" +artifactId = "okio" +version = "@okio.version@" + +[[platform.java17.dependency]] +path = "./lib/okhttp-@okhttp.version@.jar" +groupId = "com.squareup.okhttp3" +artifactId = "okhttp" +version = "@okhttp.version@" + +[[platform.java17.dependency]] +path = "./lib/kotlin-runtime-@kotlin.version@.jar" +groupId = "org.jetbrains.kotlin" +artifactId = "kotlin-runtime" +version = "@kotlin.version@" + +[[platform.java17.dependency]] +path = "./lib/kotlin-stdlib-@kotlinStdlib.version@.jar" +groupId = "org.jetbrains.kotlin" +artifactId = "kotlin-stdlib" +version = "@kotlinStdlib.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-api-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-api" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-api-metrics-@openTelemetryAlpha.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-api-metrics" +version = "@openTelemetryAlpha.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-sdk-trace-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-sdk-trace" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-sdk-common-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-sdk-common" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-semconv-@openTelemetryAlpha.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-semconv" +version = "@openTelemetryAlpha.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-proto-@openTelemetryAlpha.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-proto" +version = "@openTelemetryAlpha.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-exporter-otlp-trace-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-exporter-otlp-trace" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-exporter-otlp-common-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-exporter-otlp-common" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/opentelemetry-extension-trace-propagators-@opentelemetry.version@.jar" +groupId = "io.opentelemetry" +artifactId = "opentelemetry-extension-trace-propagators" +version = "@opentelemetry.version@" + +[[platform.java17.dependency]] +path = "./lib/guava-@guava.version@.jar" +groupId = "com.google.guava" +artifactId = "guava" +version = "@guava.version@" + +[[platform.java17.dependency]] +path = "./lib/failureaccess-@failureAccess.version@.jar" +groupId = "com.google.guava" +artifactId = "failureaccess" +version = "@failureAccess.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-api-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-api" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-context-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-context" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-core-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-core" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-stub-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-stub" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-protobuf-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-protobuf" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-protobuf-lite-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-protobuf-lite" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/grpc-netty-shaded-@grpc.version@.jar" +groupId = "io.grpc" +artifactId = "grpc-netty-shaded" +version = "@grpc.version@" + +[[platform.java17.dependency]] +path = "./lib/perfmark-api-@perfmark.version@.jar" +groupId = "io.perfmark" +artifactId = "perfmark-api" +version = "@perfmark.version@" + diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..5955a7f --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,62 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.8.3" + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" +modules = [ + {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "observe", moduleName = "observe"}, + {org = "ballerina", packageName = "observe", moduleName = "observe.mockextension"} +] + +[[package]] +org = "ballerinax" +name = "newrelic" +version = "0.8.2" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "newrelic", moduleName = "newrelic"} +] + diff --git a/ballerina/Package.md b/ballerina/Package.md new file mode 100644 index 0000000..1d8a2d0 --- /dev/null +++ b/ballerina/Package.md @@ -0,0 +1,42 @@ +## Package Overview + +The New Relic Observability Extension is one of the observability extensions in the Ballerina language. + +It provides an implementation for tracing and metrics and, publishing both metrics and traces to a New Relic platform. + +## Enabling New Relic Extension + +To package the New Relic extension into the Jar, follow the following steps. +1. Add the following import to your program. +```ballerina +import ballerinax/newrelic as _; +``` + +2. Add the following to the `Ballerina.toml` when building your program. +```toml +[package] +org = "my_org" +name = "my_package" +version = "1.0.0" + +[build-options] +observabilityIncluded=true +``` + +To enable the extension and publish traces and metrics to New Relic, add the following to the `Config.toml` when running your program. +```toml +[ballerina.observe] +tracingEnabled=true +tracingProvider="newrelic" +metricsEnabled=true +metricsReporter="newrelic" + +[ballerinax.newrelic] +apiKey="" # Mandatory Configuration. +tracingSamplerType="const" # Optional Configuration. Default value is 'const' +tracingSamplerParam=1 # Optional Configuration. Default value is 1 +tracingReporterFlushInterval=15000 # Optional Configuration. Default value is 15000 milliseconds +tracingReporterBufferSize=10000 # Optional Configuration. Default value is 10000 +metricReporterFlushInterval=15000 # Optional Configuration. Default value is 15000 milliseconds +metricReporterClientTimeout=10000 # Optional Configuration. Default value is 10000 milliseconds +``` diff --git a/ballerina/build.gradle b/ballerina/build.gradle new file mode 100644 index 0000000..ca0c30e --- /dev/null +++ b/ballerina/build.gradle @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +description = 'Ballerina - New Relic Extension - Ballerina Module' + +configurations { + nativeJar { + transitive false + } + externalJars { + transitive false + } + distribution { + canBeConsumed true + canBeResolved false + } +} + +dependencies { + nativeJar project(':newrelic-extension-native') + + externalJars "com.newrelic.telemetry:telemetry-all:${newrelicTelemetryVersion}" + externalJars "com.newrelic.telemetry:telemetry-http-okhttp:${newrelicTelemetryVersion}" + externalJars "com.squareup.okio:okio:${okioVersion}" + externalJars "com.squareup.okhttp3:okhttp:${okhttpVersion}" + externalJars "org.jetbrains.kotlin:kotlin-runtime:${kotlinVersion}" + externalJars "org.jetbrains.kotlin:kotlin-stdlib:${kotlinStdlibVersion}" + externalJars "io.opentelemetry:opentelemetry-api:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-api-metrics:${openTelemetryAlphaVersion}" + externalJars "io.opentelemetry:opentelemetry-context:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-sdk-trace:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-sdk-common:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-semconv:${openTelemetryAlphaVersion}" + externalJars "io.opentelemetry:opentelemetry-proto:${openTelemetryAlphaVersion}" + externalJars "io.opentelemetry:opentelemetry-exporter-otlp-trace:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-exporter-otlp-common:${openTelemetryVersion}" + externalJars "io.opentelemetry:opentelemetry-extension-trace-propagators:${openTelemetryVersion}" + externalJars "io.grpc:grpc-api:${grpcVersion}" + externalJars "io.grpc:grpc-context:${grpcVersion}" + externalJars "io.grpc:grpc-core:${grpcVersion}" + externalJars "io.grpc:grpc-stub:${grpcVersion}" + externalJars "io.grpc:grpc-protobuf:${grpcVersion}" + externalJars "io.grpc:grpc-protobuf-lite:${grpcVersion}" + externalJars "io.grpc:grpc-netty-shaded:${grpcVersion}" + externalJars "io.perfmark:perfmark-api:${perfmarkVersion}" + externalJars "com.google.guava:guava:${guavaVersion}" + externalJars "com.google.guava:failureaccess:${failureAccessVersion}" +} + +clean { + delete "${project.projectDir}/target" + delete "${project.projectDir}/Ballerina.lock" + delete "${project.projectDir}/ballerina-internal.log" +} + +jar { + manifest { + attributes('Implementation-Title': project.name, 'Implementation-Version': project.version) + } +} + +task copyExternalJarsToLib(type: Copy) { + into "${project.projectDir}/lib" + from configurations.externalJars +} + +def packageOrg = "ballerinax" +def packageName = "newrelic" +def ballerinaConfigFile = new File("${project.projectDir}/Ballerina.toml") +def artifactBallerinaDocs = file("${project.projectDir}/build/docs_parent/") +def artifactCacheParent = file("${project.buildDir}/cache_parent/") +def artifactLibParent = file("${project.buildDir}/lib_parent/") +def snapshotVersion = "-SNAPSHOT" +def tomlVersion = stripBallerinaExtensionVersion("${project.version}") +def ballerinaCentralAccessToken = System.getenv('BALLERINA_CENTRAL_ACCESS_TOKEN') +def originalConfig = ballerinaConfigFile.text +def artifactJar = file("$project.projectDir/target/cache/${packageOrg}/${packageName}/${tomlVersion}/java17/") +def platform = "java17" +def skipTests = false + +def stripBallerinaExtensionVersion(String extVersion) { + if (extVersion.matches(project.ext.timestampedVersionRegex)) { + def splitVersion = extVersion.split('-'); + if (splitVersion.length > 3) { + def strippedValues = splitVersion[0..-4] + return strippedValues.join('-') + } else { + return extVersion + } + } else { + return extVersion.replace("${project.ext.snapshotVersion}", "") + } +} + +task updateTomlVersions { + doLast { + def newrelicTelemetryVersion = project.newrelicTelemetryVersion + def okioVersion = project.okioVersion + def okhttpVersion = project.okhttpVersion + def kotlinVersion = project.kotlinVersion + def kotlinStdlibVersion = project.kotlinStdlibVersion + def openTelemetryVersion = project.openTelemetryVersion + def grpcVersion = project.grpcVersion + def perfmarkVersion = project.perfmarkVersion + def openTelemetryAlphaVersion = project.openTelemetryAlphaVersion + def guavaVersion = project.guavaVersion + def failureAccessVersion = project.failureAccessVersion + + def newConfig = ballerinaConfigFile.text.replace("@project.version@", project.version) + newConfig = newConfig.replace("@toml.version@", tomlVersion) + newConfig = newConfig.replace("@newrelic.telemetry.version@", newrelicTelemetryVersion) + newConfig = newConfig.replace("@okio.version@", okioVersion) + newConfig = newConfig.replace("@okhttp.version@", okhttpVersion) + newConfig = newConfig.replace("@kotlin.version@", kotlinVersion) + newConfig = newConfig.replace("@kotlinStdlib.version@", kotlinStdlibVersion) + newConfig = newConfig.replace("@opentelemetry.version@", openTelemetryVersion) + newConfig = newConfig.replace("@openTelemetryAlpha.version@", openTelemetryAlphaVersion) + newConfig = newConfig.replace("@grpc.version@", grpcVersion) + newConfig = newConfig.replace("@perfmark.version@", perfmarkVersion) + newConfig = newConfig.replace("@guava.version@", guavaVersion) + newConfig = newConfig.replace("@failureAccess.version@", failureAccessVersion) + ballerinaConfigFile.text = newConfig + } +} + +task revertTomlFile { + doLast { + ballerinaConfigFile.text = originalConfig + } +} + +task ballerinaBuild { + dependsOn updateTomlVersions + dependsOn configurations.nativeJar + dependsOn configurations.externalJars + dependsOn copyExternalJarsToLib + dependsOn compileJava + dependsOn compileTestJava + dependsOn jar + dependsOn checkstyleMain + dependsOn checkstyleTest + dependsOn spotbugsMain + dependsOn spotbugsTest + dependsOn test + dependsOn ":newrelic-extension-ballerina:generatePomFileForMavenJavaPublication" + + inputs.dir file(project.projectDir) + if (skipTests) { + finalizedBy(revertTomlFile) + } + + doLast { + def additionalBuildParams = "" + if (project.hasProperty("debug")) { + additionalBuildParams = "--debug ${project.findProperty("debug")}" + } + + // build bala file + executeBalCommand ("build", "${project.projectDir}") + executeBalCommand ("pack", "${project.projectDir}") + + // extract bala file to artifact cache directory + file("${project.projectDir}/target/bala").eachFileMatch(~/.*.bala/) { balaFile -> + copy { + from zipTree(balaFile) + into file("$artifactCacheParent/bala/${packageOrg}/${packageName}/${tomlVersion}/${platform}") + } + } + copy { + from file("${project.projectDir}/target/cache") + exclude '**/*-testable.jar' + exclude '**/tests_cache/' + into file("$artifactCacheParent/cache/") + } + + // Doc creation and packing + executeBalCommand ("doc", "${project.projectDir}") + + copy { + from file("$project.projectDir/target/apidocs/${packageName}") + into file("$project.buildDir/docs_parent/docs/${packageName}") + } + } + + outputs.dir artifactCacheParent + outputs.dir artifactBallerinaDocs + outputs.dir artifactLibParent +} + +task createArtifactZip(type: Zip) { + destinationDirectory = file("${project.buildDir}/distributions") + from ballerinaBuild +} + +artifacts { + distribution createArtifactZip +} + +task ballerinaPublish { + dependsOn updateTomlVersions + dependsOn ballerinaBuild + dependsOn ":newrelic-extension-ballerina:generatePomFileForMavenJavaPublication" + + finalizedBy(revertTomlFile) + + doLast { + if (project.version.toString().split("-").length > 1) { + return + } + if (ballerinaCentralAccessToken != null) { + println("Publishing to the ballerina central...") + def env = "JAVA_OPTS -DBALLERINA_DEV_COMPILE_BALLERINA_ORG=true" + executeBalCommand ("push", "${project.projectDir}", env) + } else { + throw new InvalidUserDataException("Central Access Token is not present") + } + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifact source: createArtifactZip, extension: 'zip' + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/${System.getenv("packagePublishRepo")}") + credentials { + username = System.getenv("packageUser") + password = System.getenv("packagePAT") + } + } + } +} + +build { + dependsOn ballerinaBuild +} + +publish { + dependsOn ballerinaPublish +} + +task extractBallerinaClassFiles(type: Copy) { + fileTree(artifactJar).forEach { file -> + from zipTree(file).matching { + exclude '**/tests/*' + include '**/*.class' + } + into "${project.rootDir.absolutePath}/build/classes" + } +} + +task publishBalaFileToLocal { + dependsOn ballerinaBuild + + finalizedBy(revertTomlFile) + + doLast { + if (!skipTests) { + exec { + workingDir "${project.rootDir}/ballerina" + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "bal.bat push --repository=local" + + " && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', '-c', "bal push --repository=local" + } + } + } + } +} diff --git a/ballerina/metrics_reporter.bal b/ballerina/metrics_reporter.bal new file mode 100644 index 0000000..41e07ec --- /dev/null +++ b/ballerina/metrics_reporter.bal @@ -0,0 +1,38 @@ +// Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you 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. + +import ballerina/jballerina.java; +import ballerina/log; + +configurable int metricReporterFlushInterval = 15000; +configurable int metricReporterClientTimeout = 10000; + +isolated function startMetricsReporter(string apiKey) { + string[] output = externSendMetrics(apiKey, metricReporterFlushInterval, metricReporterClientTimeout); + + foreach string outputLine in output { + if (outputLine.startsWith("error:")) { + log:printError(outputLine); + } else { + log:printInfo(outputLine); + } + } +} + +isolated function externSendMetrics(string apiKey,int metricReporterFlushInterval, int metricReporterClientTimeout) returns string[] = @java:Method { + 'class: "io.ballerina.observe.metrics.newrelic.NewRelicMetricsReporter", + name: "sendMetrics" +} external; diff --git a/ballerina/observe.bal b/ballerina/observe.bal new file mode 100644 index 0000000..83ccaff --- /dev/null +++ b/ballerina/observe.bal @@ -0,0 +1,35 @@ +// Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you 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. + +import ballerina/observe; + +const REPORTER_NAME = "newrelic"; +const PROVIDER_NAME = "newrelic"; + +configurable string apiKey = ""; + +function init() returns error? { + if apiKey == "" { + return error("error: cannot find API key for trace API. Please configure API key in Config.toml file."); + } else { + if observe:isTracingEnabled() && observe:getTracingProvider() == PROVIDER_NAME { + startTracerProvider(apiKey); + } + if observe:isMetricsEnabled() && observe:getMetricsReporter() == REPORTER_NAME { + startMetricsReporter(apiKey); + } + } +} diff --git a/ballerina/tracer_provider.bal b/ballerina/tracer_provider.bal new file mode 100644 index 0000000..c05125d --- /dev/null +++ b/ballerina/tracer_provider.bal @@ -0,0 +1,54 @@ +// Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you 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. + +import ballerina/log; +import ballerina/jballerina.java; + +const DEFAULT_SAMPLER_TYPE = "const"; + +configurable string tracingSamplerType = "const"; +configurable decimal tracingSamplerParam = 1; +configurable int tracingReporterFlushInterval = 15000; +configurable int tracingReporterBufferSize = 10000; + +function startTracerProvider(string apiKey) { + string selectedSamplerType; + + if tracingSamplerType != "const" && tracingSamplerType != "ratelimiting" && tracingSamplerType != "probabilistic" { + selectedSamplerType = DEFAULT_SAMPLER_TYPE; + log:printError("error: invalid New Relic configuration sampler type: " + tracingSamplerType + + ". using default " + DEFAULT_SAMPLER_TYPE + " sampling"); + } else { + selectedSamplerType = tracingSamplerType; + } + + string[] output = externStartPublishingTraces(apiKey, selectedSamplerType, tracingSamplerParam, + tracingReporterFlushInterval, tracingReporterBufferSize); + + foreach string outputLine in output { + if (outputLine.startsWith("error:")) { + log:printError(outputLine); + } else { + log:printInfo(outputLine); + } + } +} + +function externStartPublishingTraces(string apiKey, string samplerType, + decimal samplerParam, int reporterFlushInterval, int reporterBufferSize) returns string[] = @java:Method { + 'class: "io.ballerina.observe.trace.newrelic.NewRelicTracerProvider", + name: "startPublishingTraces" +} external; diff --git a/build-config/checkstyle/build.gradle b/build-config/checkstyle/build.gradle new file mode 100644 index 0000000..58eb75e --- /dev/null +++ b/build-config/checkstyle/build.gradle @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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. + */ + +plugins { + id "de.undercouch.download" +} + +task downloadFiles(type: Download) { + src([ + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/checkstyle.xml', + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/suppressions.xml' + ]) + overwrite false + onlyIfNewer true + dest buildDir +} + +jar { + enabled = false +} + +clean { + enabled = false +} + +artifacts.add('default', file("$project.buildDir/checkstyle.xml")) { + builtBy('downloadFiles') +} + +artifacts.add('default', file("$project.buildDir/suppressions.xml")) { + builtBy('downloadFiles') +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3c18db7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +plugins { + id 'checkstyle' + id "com.github.spotbugs" version "5.0.14" + id "net.researchgate.release" version "2.8.0" + id "de.undercouch.download" version "5.4.0" + id "jacoco" +} + +allprojects { + group = project.group + version = project.version + + apply plugin: 'maven-publish' + + repositories { + mavenLocal() + mavenCentral() + + maven { + url = 'https://maven.wso2.org/nexus/content/repositories/releases/' + } + maven { + url = 'https://maven.wso2.org/nexus/content/groups/wso2-public/' + } + maven { + url = 'https://repo.maven.apache.org/maven2' + } + maven { + url 'https://maven.pkg.github.com/ballerina-platform/*' + credentials { + username System.getenv('packageUser') + password System.getenv('packagePAT') + } + } + } + + ext { + snapshotVersion= '-SNAPSHOT' + timestampedVersionRegex = '.*-\\d{8}-\\d{6}-\\w.*\$' + } +} + +subprojects { + apply plugin: 'java-library' + apply plugin: 'checkstyle' + apply plugin: 'com.github.spotbugs' + + configurations { + jbalTools { + transitive = false + } + ballerinaStdLibs + } + + checkstyle { + toolVersion '10.12.1' + } + + spotbugsMain { + it.effort "max" + it.reportLevel "low" + it.reports { + xml.enabled false + html.enabled true + } + def excludeFile = file('spotbugs-exclude.xml') + if (excludeFile.exists()) { + it.excludeFilter = excludeFile + } + } + + spotbugsTest { + it.effort "max" + it.reportLevel "low" + it.reports { + xml.enabled false + html.enabled true + } + def excludeFile = file('spotbugs-test-exclude.xml') + if (excludeFile.exists()) { + it.excludeFilter = excludeFile + } + } + + tasks.withType(Checkstyle) { + dependsOn ":build-config:checkstyle:downloadFiles" + configFile rootProject.file("build-config/checkstyle/build/checkstyle.xml") + exclude '**/module-info.java' + } +} + +def moduleVersion = project.version.replace("-SNAPSHOT", "") + +def executeBalCommand(String command, String dir, env = "") { + try { + exec { + workingDir dir + environment environment: env + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "bal.bat ${command} && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', '-c', "bal ${command}" + } + } + } catch (Exception e) { + println("bal command failed. " + e.message) + throw e + } +} + +release { + failOnPublishNeeded = false + failOnSnapshotDependencies = true + + buildTasks = ['build'] + versionPropertyFile = 'gradle.properties' + tagTemplate = 'v$version' + + git { + requireBranch = "release-${moduleVersion}" + pushToRemote = 'origin' + } +} + +task build { + dependsOn('newrelic-extension-ballerina:build') +} + +task codeCoverageReport(type: JacocoReport) { + dependsOn('newrelic-extension-ballerina:extractBallerinaClassFiles') + dependsOn('newrelic-extension-native:copyJavaClassFiles') + + executionData fileTree(project.rootDir.absolutePath).include("**/*.exec") + additionalClassDirs files("${buildDir}/classes") + + subprojects.each { + sourceSets it.sourceSets.main + } + + reports { + xml.required = true + html.required = true + xml.destination = new File("${buildDir}/reports/jacoco/report.xml") + html.destination = new File("${buildDir}/reports/jacoco/report.html") + } + + onlyIf = { + true + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..13f2471 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +fixes: + - "ballerinax/newrelic/*/::ballerina/" + +ignore: + - "**/test" diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e7b8045 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,38 @@ +# Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +# +# WSO2 LLC. licenses this file to you 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. + +group=org.ballerinalang +version=0.7.0-SNAPSHOT +ballerinaLangVersion=2201.8.2 +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.jvmargs='-Dfile.encoding=UTF-8' +org.gradle.workers.max=3 + +# Native Dependency Versions +newrelicTelemetryVersion=0.16.0 +okioVersion=3.5.0 +okhttpVersion=4.11.0 +kotlinVersion=1.2.71 +kotlinStdlibVersion=1.8.22 +openTelemetryVersion=1.0.0 +openTelemetryAlphaVersion=1.0.0-alpha +grpcVersion=1.35.0 +protobufVersion=3.20.3 +perfmarkVersion=0.23.0 +guavaVersion=32.1.2-jre +failureAccessVersion=1.0.1 + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9f4197d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f7ee8dd --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +# +# WSO2 LLC. licenses this file to you 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. + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ea54f13 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +@rem +@rem WSO2 LLC. licenses this file to you under the Apache License, +@rem Version 2.0 (the "License"); you may not use this file except +@rem in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/native/build.gradle b/native/build.gradle new file mode 100644 index 0000000..ac6b510 --- /dev/null +++ b/native/build.gradle @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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. + */ + +plugins { + id 'java-library' +} + +description = 'Ballerina - New Relic Extension - Native Module' + +dependencies { + implementation "org.ballerinalang:ballerina-runtime:${ballerinaLangVersion}" + implementation "com.newrelic.telemetry:telemetry-all:${newrelicTelemetryVersion}" + implementation "com.newrelic.telemetry:telemetry-http-okhttp:${newrelicTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-api:${openTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-api-metrics:${openTelemetryAlphaVersion}" + implementation "io.opentelemetry:opentelemetry-context:${openTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-sdk-trace:${openTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-sdk-common:${openTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-semconv:${openTelemetryAlphaVersion}" + implementation "io.opentelemetry:opentelemetry-exporter-otlp-trace:${openTelemetryVersion}" + implementation "io.opentelemetry:opentelemetry-extension-trace-propagators:${openTelemetryVersion}" + implementation("com.google.guava:guava:${guavaVersion}") { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + } + implementation("com.google.guava:failureaccess:${failureAccessVersion}") +} + +jar { + manifest { + attributes('Implementation-Title': project.name, 'Implementation-Version': project.version) + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.group + artifactId "newrelic-extension-native" + version = project.version + artifact jar + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/${System.getenv("packagePublishRepo")}") + credentials { + username = System.getenv("packageUser") + password = System.getenv("packagePAT") + } + } + } +} + +task copyJavaClassFiles(type: Copy) { + dependsOn(compileJava) + from("${project.buildDir}/classes") { + exclude '**/module-info.class' + include '**/*.class' + } + into "${project.rootDir.absolutePath}/build/classes" +} diff --git a/native/src/main/java/io/ballerina/observe/metrics/newrelic/NewRelicMetricsReporter.java b/native/src/main/java/io/ballerina/observe/metrics/newrelic/NewRelicMetricsReporter.java new file mode 100644 index 0000000..dd9682a --- /dev/null +++ b/native/src/main/java/io/ballerina/observe/metrics/newrelic/NewRelicMetricsReporter.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.observe.metrics.newrelic; + +import com.newrelic.telemetry.Attributes; +import com.newrelic.telemetry.OkHttpPoster; +import com.newrelic.telemetry.TelemetryClient; +import com.newrelic.telemetry.metrics.Count; +import com.newrelic.telemetry.metrics.MetricBatch; +import com.newrelic.telemetry.metrics.MetricBuffer; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.internal.types.BStringType; +import io.ballerina.runtime.observability.metrics.Counter; +import io.ballerina.runtime.observability.metrics.DefaultMetricRegistry; +import io.ballerina.runtime.observability.metrics.Gauge; +import io.ballerina.runtime.observability.metrics.Metric; +import io.ballerina.runtime.observability.metrics.MetricConstants; +import io.ballerina.runtime.observability.metrics.MetricId; +import io.ballerina.runtime.observability.metrics.PercentileValue; +import io.ballerina.runtime.observability.metrics.PolledGauge; +import io.ballerina.runtime.observability.metrics.Snapshot; +import io.ballerina.runtime.observability.metrics.Tag; + +import java.io.PrintStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static io.ballerina.observe.metrics.newrelic.ObserveNativeImplConstants.EXPIRY_TAG; +import static io.ballerina.observe.metrics.newrelic.ObserveNativeImplConstants.PERCENTILE_TAG; + +/** + * This is the New Relic metric reporter class. + */ +public class NewRelicMetricsReporter { + private static final String METRIC_REPORTER_ENDPOINT = "https://metric-api.newrelic.com/metric/v1"; + private static final int SCHEDULE_EXECUTOR_INITIAL_DELAY = 0; + + public static BArray sendMetrics(BString apiKey, int metricReporterFlushInterval, + int metricReporterClientTimeout) { + BArray output = ValueCreator.createArrayValue(TypeCreator.createArrayType(PredefinedTypes.TYPE_STRING)); + + // create a TelemetryClient with an HTTP connect timeout of 10 seconds. + TelemetryClient telemetryClient = + TelemetryClient.create( + () -> new OkHttpPoster(Duration.of(metricReporterClientTimeout, ChronoUnit.MILLIS)), + apiKey.getValue()); + Attributes commonAttributes = null; + try { + commonAttributes = new Attributes() + .put("host", InetAddress.getLocalHost().getHostName()) + .put("language", "ballerina"); + } catch (UnknownHostException e) { + output.append(StringUtils.fromString("error: while getting the host name of the instance")); + } + + // Create a ScheduledExecutorService with a single thread + ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + // Schedule a task to run every 1 second with an initial delay of 0 seconds + Attributes finalCommonAttributes = commonAttributes; + executorService.scheduleAtFixedRate(() -> { + MetricBuffer metricBuffer = generateMetricBuffer(finalCommonAttributes); + MetricBatch batch = metricBuffer.createBatch(); + telemetryClient.sendBatch(batch); + }, SCHEDULE_EXECUTOR_INITIAL_DELAY, metricReporterFlushInterval, TimeUnit.MILLISECONDS); + + output.append(StringUtils.fromString("ballerina: started publishing metrics to New Relic on " + + METRIC_REPORTER_ENDPOINT)); + + return output; + } + + private static MetricBuffer generateMetricBuffer(Attributes commonAttributes) { + MetricBuffer metricBuffer = new MetricBuffer(commonAttributes); + Metric[] metrics = DefaultMetricRegistry.getInstance().getAllMetrics(); + + for (Metric metric : metrics) { + MetricId metricId = metric.getId(); + String qualifiedMetricName = metricId.getName(); + String metricReportName = getMetricName(qualifiedMetricName, "value"); + + Double metricValue = null; + String metricType = null; + Snapshot[] snapshots = null; + + if (metric instanceof Counter counter) { + metricValue = getMetricValue(counter.getValue()); + metricType = MetricConstants.COUNTER; + } else if (metric instanceof Gauge gauge) { + metricValue = getMetricValue(gauge.getValue()); + metricType = MetricConstants.GAUGE; + snapshots = gauge.getSnapshots(); + } else if (metric instanceof PolledGauge polledGauge) { + metricValue = getMetricValue(polledGauge.getValue()); + metricType = MetricConstants.GAUGE; + } + if (metricValue != null) { + long startTimeInMillis = System.currentTimeMillis(); + Attributes tags = new Attributes(); + for (Tag tag : metricId.getTags()) { + tags.put(tag.getKey(), tag.getValue()); + } + + if (metricType.equals(MetricConstants.COUNTER)) { + Count countMetric = generateCountMetric(metricReportName, metricValue, startTimeInMillis, tags); + metricBuffer.addMetric(countMetric); + } else if (metricType.equals(MetricConstants.GAUGE)) { + com.newrelic.telemetry.metrics.Gauge gaugeMetric = generateGaugeMetric(metricReportName, + metricValue, tags); + metricBuffer.addMetric(gaugeMetric); + } + + if (snapshots != null) { + for (Snapshot snapshot : snapshots) { + Attributes snapshotTags = tags.copy(); + snapshotTags.put(EXPIRY_TAG, snapshot.getTimeWindow().toString()); + metricBuffer.addMetric(generateGaugeMetric(getMetricName(qualifiedMetricName, "min"), + snapshot.getMin(), snapshotTags)); + metricBuffer.addMetric(generateGaugeMetric(getMetricName(qualifiedMetricName, "max"), + snapshot.getMax(), snapshotTags)); + metricBuffer.addMetric(generateGaugeMetric(getMetricName(qualifiedMetricName, "mean"), + snapshot.getMean(), snapshotTags)); + metricBuffer.addMetric(generateGaugeMetric(getMetricName(qualifiedMetricName, "stdDev"), + snapshot.getStdDev(), snapshotTags)); + for (PercentileValue percentileValue : snapshot.getPercentileValues()) { + Attributes percentileTags = snapshotTags.copy(); + percentileTags.put(PERCENTILE_TAG, percentileValue.getPercentile()); + metricBuffer.addMetric(generateGaugeMetric(qualifiedMetricName, percentileValue.getValue(), + percentileTags)); + } + } + } + } + } + + return metricBuffer; + } + + private static String getMetricName(String metricId, String summaryType) { + return metricId + "_" + summaryType; + } + + private static com.newrelic.telemetry.metrics.Gauge generateGaugeMetric(String metricName, double value, + Attributes tags) { + return new com.newrelic.telemetry.metrics.Gauge( + metricName, + value, + System.currentTimeMillis(), + tags); + } + + private static Count generateCountMetric(String metricName, double value, long startTime, Attributes tags) { + return new Count( + metricName, + value, + startTime, + System.currentTimeMillis(), + tags); + } + + private static double getMetricValue(Object value) { + double metricValue = 0.0; + if (value instanceof Long) { + metricValue = ((Long) value).doubleValue(); + } else if (value instanceof Integer) { + metricValue = ((Integer) value).doubleValue(); + } else if (value instanceof Double) { + metricValue = (Double) value; + } else if (value instanceof Float) { + metricValue = ((Float) value).doubleValue(); + } + return metricValue; + } +} diff --git a/native/src/main/java/io/ballerina/observe/metrics/newrelic/ObserveNativeImplConstants.java b/native/src/main/java/io/ballerina/observe/metrics/newrelic/ObserveNativeImplConstants.java new file mode 100644 index 0000000..2fb3e66 --- /dev/null +++ b/native/src/main/java/io/ballerina/observe/metrics/newrelic/ObserveNativeImplConstants.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.observe.metrics.newrelic; + +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BString; + +/** + * Constants used in Ballerina Observe package. + * + * @since 0.980.0 + */ +public final class ObserveNativeImplConstants { + + private ObserveNativeImplConstants() { + } + public static final String METRIC_REPORTER_NAME = "newrelic"; + + public static final String EXPIRY_TAG = "timeWindow"; + public static final String PERCENTILE_TAG = "quantile"; + + public static final String GAUGE = "Gauge"; + public static final String COUNTER = "Counter"; + public static final String SNAPSHOT = "Snapshot"; + public static final String METRIC = "Metric"; + public static final String STATISTIC_CONFIG = "StatisticConfig"; + public static final String PERCENTILE_VALUE = "PercentileValue"; + public static final String METRIC_NATIVE_INSTANCE_KEY = "__metric_native_instance__"; + + public static final BString NAME_FIELD = StringUtils.fromString("name"); + public static final BString DESCRIPTION_FIELD = StringUtils.fromString("description"); + public static final BString TAGS_FIELD = StringUtils.fromString("metricTags"); + public static final BString STATISTICS_CONFIG_FIELD = StringUtils.fromString("statisticConfigs"); + public static final BString EXPIRY_FIELD = StringUtils.fromString("timeWindow"); + public static final BString BUCKETS_FIELD = StringUtils.fromString("buckets"); + public static final BString PERCENTILES_FIELD = StringUtils.fromString("percentiles"); +} diff --git a/native/src/main/java/io/ballerina/observe/trace/newrelic/NewRelicTracerProvider.java b/native/src/main/java/io/ballerina/observe/trace/newrelic/NewRelicTracerProvider.java new file mode 100644 index 0000000..810b0da --- /dev/null +++ b/native/src/main/java/io/ballerina/observe/trace/newrelic/NewRelicTracerProvider.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.observe.trace.newrelic; + +import io.ballerina.observe.trace.newrelic.sampler.RateLimitingSampler; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BDecimal; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.observability.tracer.spi.TracerProvider; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; + +import java.io.PrintStream; +import java.util.concurrent.TimeUnit; + +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; + +/** + * This is the New Relic tracing extension class for {@link TracerProvider}. + */ +public class NewRelicTracerProvider implements TracerProvider { + private static final String TRACER_NAME = "newrelic"; + private static final String CONST_SAMPLER_TYPE = "const"; + private static final String PROBABILISTIC_SAMPLER_TYPE = "probabilistic"; + private static final String TRACE_REPORTER_ENDPOINT = "https://otlp.nr-data.net:4317"; + private static final String TRACE_API_KEY_HEADER = "Api-Key"; + + static SdkTracerProviderBuilder tracerProviderBuilder; + + @Override + public String getName() { + return TRACER_NAME; + } + + @Override + public void init() { + // Do Nothing + } + + public static BArray startPublishingTraces(BString apiKey, BString samplerType, BDecimal samplerParam, + int reporterFlushInterval, int reporterBufferSize) { + BArray output = ValueCreator.createArrayValue(TypeCreator.createArrayType(PredefinedTypes.TYPE_STRING)); + + OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder() + .setEndpoint(TRACE_REPORTER_ENDPOINT) + .addHeader(TRACE_API_KEY_HEADER, apiKey.getValue()) + .build(); + + tracerProviderBuilder = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor + .builder(exporter) + .setMaxExportBatchSize(reporterBufferSize) + .setExporterTimeout(reporterFlushInterval, TimeUnit.MILLISECONDS) + .build()); + + tracerProviderBuilder.setSampler(selectSampler(samplerType, samplerParam)); + output.append(StringUtils.fromString("ballerina: started publishing traces to New Relic on " + + TRACE_REPORTER_ENDPOINT)); + + return output; + } + + private static Sampler selectSampler(BString samplerType, BDecimal samplerParam) { + switch (samplerType.getValue()) { + default: + case CONST_SAMPLER_TYPE: + if (samplerParam.value().intValue() == 0) { + return Sampler.alwaysOff(); + } else { + return Sampler.alwaysOn(); + } + case PROBABILISTIC_SAMPLER_TYPE: + return Sampler.traceIdRatioBased(samplerParam.value().doubleValue()); + case RateLimitingSampler.TYPE: + return new RateLimitingSampler(samplerParam.value().intValue()); + } + } + + @Override + public Tracer getTracer(String serviceName) { + + return tracerProviderBuilder.setResource( + Resource.create(Attributes.of(SERVICE_NAME, serviceName))) + .build().get(TRACER_NAME); + } + + @Override + public ContextPropagators getPropagators() { + return ContextPropagators.create(B3Propagator.injectingSingleHeader()); + } +} diff --git a/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimiter.java b/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimiter.java new file mode 100644 index 0000000..a2e9d7b --- /dev/null +++ b/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimiter.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.ballerina.observe.trace.newrelic.sampler; + +import io.opentelemetry.sdk.common.Clock; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is copied from https://github.com/open-telemetry/opentelemetry-java/blob/v1.0.0/sdk-extensions/ + * jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimiter.java. + */ +class RateLimiter { + private final Clock clock; + private final double creditsPerNanosecond; + private final long maxBalance; // max balance in nano ticks + private final AtomicLong debit; // last op nano time less remaining balance + + RateLimiter(double creditsPerSecond, double maxBalance, Clock clock) { + this.clock = clock; + this.creditsPerNanosecond = creditsPerSecond / 1.0e9; + this.maxBalance = (long) (maxBalance / creditsPerNanosecond); + this.debit = new AtomicLong(clock.nanoTime() - this.maxBalance); + } + + public boolean checkCredit(double itemCost) { + long cost = (long) (itemCost / creditsPerNanosecond); + long credit; + long currentDebit; + long balance; + do { + currentDebit = debit.get(); + credit = clock.nanoTime(); + balance = credit - currentDebit; + if (balance > maxBalance) { + balance = maxBalance; + } + balance -= cost; + if (balance < 0) { + return false; + } + } while (!debit.compareAndSet(currentDebit, credit - balance)); + return true; + } +} diff --git a/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimitingSampler.java b/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimitingSampler.java new file mode 100644 index 0000000..8c320d7 --- /dev/null +++ b/native/src/main/java/io/ballerina/observe/trace/newrelic/sampler/RateLimitingSampler.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.ballerina.observe.trace.newrelic.sampler; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.internal.SystemClock; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; + +import java.util.List; + +/** + * This class is copied from https://github.com/open-telemetry/opentelemetry-java/blob/v1.0.0/sdk-extensions/ + * jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSampler.java. + * This sampler uses a leaky bucket rate limiter to ensure that traces are sampled with a certain constant rate. + */ +public class RateLimitingSampler implements Sampler { + public static final String TYPE = "ratelimiting"; + + private final double maxTracesPerSecond; + private final RateLimiter rateLimiter; + private final SamplingResult onSamplingResult; + private final SamplingResult offSamplingResult; + + /** + * Creates rate limiting sampler. + * + * @param maxTracesPerSecond the maximum number of sampled traces per second. + */ + public RateLimitingSampler(int maxTracesPerSecond) { + this.maxTracesPerSecond = maxTracesPerSecond; + double maxBalance = maxTracesPerSecond < 1.0 ? 1.0 : maxTracesPerSecond; + this.rateLimiter = new RateLimiter(maxTracesPerSecond, maxBalance, SystemClock.getInstance()); + Attributes attributes = Attributes.empty(); + this.onSamplingResult = SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE, attributes); + this.offSamplingResult = SamplingResult.create(SamplingDecision.DROP, attributes); + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return this.rateLimiter.checkCredit(1.0) ? onSamplingResult : offSamplingResult; + } + + @Override + public String getDescription() { + return String.format("RateLimitingSampler{%.2f}", maxTracesPerSecond); + } + + @Override + public String toString() { + return getDescription(); + } + + // Visible for testing + double getMaxTracesPerSecond() { + return maxTracesPerSecond; + } +} diff --git a/native/src/main/resources/META-INF/services/io.ballerina.runtime.observability.tracer.spi.TracerProvider b/native/src/main/resources/META-INF/services/io.ballerina.runtime.observability.tracer.spi.TracerProvider new file mode 100644 index 0000000..3b2c235 --- /dev/null +++ b/native/src/main/resources/META-INF/services/io.ballerina.runtime.observability.tracer.spi.TracerProvider @@ -0,0 +1,17 @@ +# Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +# +# WSO2 LLC. licenses this file to you 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. + +io.ballerina.observe.trace.newrelic.NewRelicTracerProvider diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b7494cf --- /dev/null +++ b/settings.gradle @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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. + */ + +plugins { + id "com.gradle.enterprise" version "3.2" +} + +rootProject.name = 'module-ballerinax-newrelic' + +include(':build-config:checkstyle') +include(':newrelic-extension-ballerina') +include(':newrelic-extension-native') +//include(':newrelic-extension-tests') + +project(':build-config:checkstyle').projectDir = file('build-config/checkstyle') +project(':newrelic-extension-ballerina').projectDir = file('ballerina') +project(':newrelic-extension-native').projectDir = file('native') +//project(':newrelic-extension-tests').projectDir = file('tests') + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +}