diff --git a/README.md b/README.md index 77e0f0db..7a340040 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ OpenTelemetry is a big ecosystem and everything doesn't fit into the goals of th We encourage your contributions to improve `newrelic-opentelemetry-examples`! Keep in mind that when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. You only have to sign the CLA one time per project. -Generally, we want to focus on the [getting started guides](#getting-started-guides). We're open to additional examples being added which are aligned with the [demo app specification](./getting-started-guides/demo-app-specification.md) and which have a volunteer [codeowner](#codeowners). +Generally, we want to focus on the [getting started guides](#getting-started-guides). We're open to additional examples being added which have a volunteer [codeowner](#codeowners). We're more selective about additions to [other examples](#other-examples). We use the following criteria to evaluate additions: diff --git a/getting-started-guides/.dockerignore b/getting-started-guides/.dockerignore new file mode 100644 index 00000000..dc157ff1 --- /dev/null +++ b/getting-started-guides/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +LICENSE +README.md diff --git a/getting-started-guides/.env b/getting-started-guides/.env new file mode 100644 index 00000000..eb5da8c4 --- /dev/null +++ b/getting-started-guides/.env @@ -0,0 +1,7 @@ +OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net +OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_API_KEY_HERE" + +OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095 +OTEL_EXPORTER_OTLP_COMPRESSION=gzip +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=DELTA diff --git a/getting-started-guides/README.md b/getting-started-guides/README.md index 592fc5d9..39f6e147 100644 --- a/getting-started-guides/README.md +++ b/getting-started-guides/README.md @@ -1,12 +1,26 @@ -# Getting Started Guides +# Getting Started with New Relic using OpenTelemetry -This repo holds the source code for the demo applications used in the [Getting Started Guides](https://docs.newrelic.com/docs/more-integrations/open-source-telemetry-integrations/opentelemetry/get-started/opentelemetry-get-started-intro/). +The examples within this directory demonstrate how to send data to New Relic +using OpenTelemetry. -Each language directory illustrates how to add OpenTelemetry instrumentation to a simple web application, and configure OpenTelemetry for an optimal New Relic experience. This includes exporting over OTLP, limiting attributes according to New Relic ingest limits, and more. +Each language directory illustrates how to add OpenTelemetry instrumentation to +a simple web application and configure OpenTelemetry for an optimal New Relic +experience. This includes exporting over OTLP, limiting attributes according to +New Relic ingest limits, and more. -In order to provide some degree of uniformity, each sample app is written to comply with the [demo app specification](./demo-app-specification.md). Each language contains the following sub-directories: +To get started quickly, you can use Docker Compose to spin up all the example +applications. -* **Uninstrumented:** Contains the uninstrumented version of the app. -* **Instrumented:** Contains the instrumented versions of the app. The uninstrumented app is enhanced with OpenTelemetry based instrumentation to generate metrics, logs, and traces, and SDK configuration aligned with New Relic [best practices](https://docs.newrelic.com/docs/more-integrations/open-source-telemetry-integrations/opentelemetry/best-practices/opentelemetry-best-practices-overview/) to export telemetry to New Relic. +1. First, open the [.env](./.env) file and configure your New Relic API key. + If necessary, also change the + [New Relic OTLP endpoint](https://docs.newrelic.com/docs/more-integrations/open-source-telemetry-integrations/opentelemetry/best-practices/opentelemetry-otlp/#configure-endpoint-port-protocol) + to match your region and needs. -To run, please see the [Getting Started Guides](https://docs.newrelic.com/docs/more-integrations/open-source-telemetry-integrations/opentelemetry/get-started/opentelemetry-get-started-intro/) documentation and follow the instructions in the README of the root of each respective sample app. +2. Then, run Docker Compose + + ```shell + docker compose up --build + ``` + +3. Lastly, go view your data in New Relic. Running using Docker Compose also + starts a simple load generator, so data should be flowing. diff --git a/getting-started-guides/compose.yaml b/getting-started-guides/compose.yaml new file mode 100644 index 00000000..a95705f1 --- /dev/null +++ b/getting-started-guides/compose.yaml @@ -0,0 +1,57 @@ +services: + dotnet: + build: dotnet + environment: + - OTEL_SERVICE_NAME=getting-started-dotnet + - OTEL_EXPORTER_OTLP_ENDPOINT + - OTEL_EXPORTER_OTLP_HEADERS + - OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095 + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=123 + ports: + - 8080 + go: + build: go + environment: + - OTEL_SERVICE_NAME=getting-started-go + - OTEL_EXPORTER_OTLP_ENDPOINT + - OTEL_EXPORTER_OTLP_HEADERS + - OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095 + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=123 + ports: + - 8080 + javascript: + build: javascript + environment: + - OTEL_SERVICE_NAME=getting-started-javascript + - OTEL_EXPORTER_OTLP_ENDPOINT + - OTEL_EXPORTER_OTLP_HEADERS + - OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095 + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=123 + ports: + - 8080 + ruby: + build: ruby + environment: + - OTEL_SERVICE_NAME=getting-started-ruby + - OTEL_EXPORTER_OTLP_ENDPOINT + - OTEL_EXPORTER_OTLP_HEADERS + - OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095 + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=123 + ports: + - 8080 + envoy: + image: envoyproxy/envoy:v1.29.4 + ports: + - "8080:8080" + volumes: + - ./supporting-files/envoy.yaml:/etc/envoy/envoy.yaml + loadgenerator: + image: python:3.12.3 + command: ["python", "/loadgenerator.py"] + volumes: + - ./supporting-files/loadgenerator.py:/loadgenerator.py + depends_on: + - dotnet + - go + - javascript + - ruby diff --git a/getting-started-guides/demo-app-specification.md b/getting-started-guides/demo-app-specification.md deleted file mode 100644 index 5c6d2e21..00000000 --- a/getting-started-guides/demo-app-specification.md +++ /dev/null @@ -1,63 +0,0 @@ -## Specification for the Getting Started Guide demo applications: - -### Application -1. Must use port 8080 - -2. User must be able to access the endpoint http://localhost:8080/fibonacci?n=[input], and endpoint should return the following JSON response: - * For valid input, `{"n":5,"result":5}` - * For invalid input, `{"message":"n must be 1 <= n <= 90."}` - -3. Must configure the OpenTelemetry SDK according to New Relic best practices. - * Should use be kept up to date with latest version of OpenTelemetry API / SDK for language. - * Should export data to New Relic using OTLP, preferring `http/protobuf` where there is no clear preference. - * Should enable gzip compression. - * Should configure metrics w/ delta temporality. - * Should configure attribute limits. - * Should use the standard environment variables for configuration rather than programmatic or other means: - * `OTEL_SERVICE_NAME=getting-started-java` - * `OTEL_EXPORTER_OTLP_HEADERS=api-key=` - * `OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net` - * `OTEL_EXPORTER_OTLP_COMPRESSION=gzip` - * `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` - * `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=DELTA` - * `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095` - -4. Must accept input as `n`, with the valid input range as 1 <= n <= 90 -5. The application must emit the following telemetry: - * Traces - * Root span - * The root span may be emitted manually when the endpoint is invoked or preferrably by instrumentation if available in the language. - * Child span - * This span should represent the fibonacci calculation. - * Span name = `fibonacci` - * Span kind = internal - * Attributes - * `fibonacci.n`, with the value of `n` representing the user's input - * `fibonacci.result`, with the value of `result` representing the result of the user’s input - * Span event - * When an error occurs, an exception event should be added as follows: - * Span status = `ERROR` - * Status description: `n must be 1 <= n <= 90.` - * Metrics - * Counter named`fibonacci.invocations` - * Attributes - * Counter description: `Measures the number of times the fibonacci method is invoked.` - * `fibonacci.valid.n`, with a boolean value indicating whether `n` was valid or not - * Logs - * Output the following when `n` is valid: `Compute fibonacci(n) = result` - * Output the following when `n` is invalid: `Failed to compute fibonacci(n)` - -### File structure -1. A directory named `Instrumented` that includes the following: - * README (spec to follow; for now, use [Getting Started Guide - Java](https://github.com/newrelic/newrelic-opentelemetry-examples/blob/main/getting-started-guides/java/instrumented/README.md) as an example) - * Source code - * Instrumentation files - * Load generator files - * `call-app.sh` and `load-generator.sh` - * `call-app.ps1` and `load-generator.ps1` -2. A directory named `Uninstrumented` that includes the following: - * README (spec to follow) - * Source code - * Load generator files - * `call-app.sh` and `load-generator.sh` - * `call-app.ps1` and `load-generator.ps1` \ No newline at end of file diff --git a/getting-started-guides/dotnet/Dockerfile b/getting-started-guides/dotnet/Dockerfile new file mode 100644 index 00000000..57bbde5b --- /dev/null +++ b/getting-started-guides/dotnet/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build + +COPY . /source + +WORKDIR /source + +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /root/.nuget/packages so that subsequent builds don't have to re-download packages. +# If TARGETARCH is "amd64", replace it with "x64" - "x64" is .NET's canonical name for this and "amd64" doesn't +# work in .NET 6.0. +RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \ + dotnet publish -a ${TARGETARCH/amd64/x64} --use-current-runtime --self-contained false -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final + +WORKDIR /app + +COPY --from=build /app . + +USER $APP_UID + +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "dotnet.dll"] diff --git a/getting-started-guides/dotnet/appsettings.Development.json b/getting-started-guides/dotnet/appsettings.Development.json index b73bf368..8a54844a 100644 --- a/getting-started-guides/dotnet/appsettings.Development.json +++ b/getting-started-guides/dotnet/appsettings.Development.json @@ -2,7 +2,7 @@ "Kestrel": { "Endpoints": { "Localhost": { - "Url": "http://localhost:8080" + "Url": "http://*:8080" } } }, diff --git a/getting-started-guides/dotnet/appsettings.json b/getting-started-guides/dotnet/appsettings.json index a939c0c9..191f4dfe 100644 --- a/getting-started-guides/dotnet/appsettings.json +++ b/getting-started-guides/dotnet/appsettings.json @@ -2,7 +2,7 @@ "Kestrel": { "Endpoints": { "Localhost": { - "Url": "http://localhost:8080" + "Url": "http://*:8080" } } }, diff --git a/getting-started-guides/go/Dockerfile b/getting-started-guides/go/Dockerfile new file mode 100644 index 00000000..2ba3aa5d --- /dev/null +++ b/getting-started-guides/go/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 + +ARG GO_VERSION=1.20 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +ARG TARGETARCH + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +FROM alpine:latest AS final + +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +COPY --from=build /bin/server /bin/ + +EXPOSE 8080 + +ENTRYPOINT [ "/bin/server" ] diff --git a/getting-started-guides/javascript/Dockerfile b/getting-started-guides/javascript/Dockerfile new file mode 100644 index 00000000..378a2ab0 --- /dev/null +++ b/getting-started-guides/javascript/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 + +ARG NODE_VERSION=20.10.0 + +FROM node:${NODE_VERSION}-alpine + +ENV NODE_ENV production + +WORKDIR /usr/src/app + +RUN --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=package-lock.json,target=package-lock.json \ + --mount=type=cache,target=/root/.npm \ + npm ci --omit=dev + +USER node + +COPY . . + +EXPOSE 8080 + +CMD npm start diff --git a/getting-started-guides/ruby/Dockerfile b/getting-started-guides/ruby/Dockerfile new file mode 100644 index 00000000..d9f003ae --- /dev/null +++ b/getting-started-guides/ruby/Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 + +FROM ruby:3.2.2 as base + +WORKDIR /usr/src/app + +COPY . . + +RUN bundle install + +EXPOSE 8080 + +CMD bundle exec rackup diff --git a/getting-started-guides/ruby/config.ru b/getting-started-guides/ruby/config.ru index 83c8e5ce..6f18c141 100644 --- a/getting-started-guides/ruby/config.ru +++ b/getting-started-guides/ruby/config.ru @@ -8,4 +8,4 @@ require './opentelemetry' require './fibonacci' require './app' -Rack::Handler.default.run(App, :Port => 8080) +Rack::Handler.default.run(App, :Host => "0.0.0.0", :Port => 8080) diff --git a/getting-started-guides/supporting-files/envoy.yaml b/getting-started-guides/supporting-files/envoy.yaml new file mode 100644 index 00000000..56725b60 --- /dev/null +++ b/getting-started-guides/supporting-files/envoy.yaml @@ -0,0 +1,82 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: AUTO + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: frontend + domains: + - "*" + routes: + - match: { prefix: "/dotnet/fibonacci" } + route: { cluster: dotnet, prefix_rewrite: "/fibonacci" } + - match: { prefix: "/go/fibonacci" } + route: { cluster: go, prefix_rewrite: "/fibonacci" } + - match: { prefix: "/javascript/fibonacci" } + route: { cluster: javascript, prefix_rewrite: "/fibonacci" } + - match: { prefix: "/ruby/fibonacci" } + route: { cluster: ruby, prefix_rewrite: "/fibonacci" } + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: dotnet + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: dotnet + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: dotnet + port_value: 8080 + - name: go + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: go + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: go + port_value: 8080 + - name: javascript + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: javascript + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: javascript + port_value: 8080 + - name: ruby + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: ruby + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ruby + port_value: 8080 diff --git a/getting-started-guides/supporting-files/loadgenerator.py b/getting-started-guides/supporting-files/loadgenerator.py new file mode 100644 index 00000000..0748dcea --- /dev/null +++ b/getting-started-guides/supporting-files/loadgenerator.py @@ -0,0 +1,15 @@ +import os, random, sys, signal, time + +def signal_handler(signal, frame): + print("\nCtrl-C received. Stopping load generator.") + sys.exit(0) + +signal.signal(signal.SIGINT, signal_handler) + +languages = ["dotnet", "go", "javascript", "ruby"] + +while True: + n = random.randint(1, 100) + for language in languages: + os.system(f"curl http://{language}:8080/fibonacci?n={n} > /dev/null 2>&1") + time.sleep(1)