Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill otel demo with more data required before kubecon #2127

Closed
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
cd70b67
[featureflag] expose feature flag API via frontend
basti1302 Jan 29, 2024
e5ce4e8
[dash0] use images from AWS ECR repository
basti1302 Jan 29, 2024
aefb610
[dash0] remove qemu support
basti1302 Jan 29, 2024
99705c7
[dash0] temporarily allow publishing images on demand
basti1302 Jan 31, 2024
0f55509
[dash0] remove workflow dispatch trigger, publish on release again
basti1302 Jan 31, 2024
c6d237c
[featureflag] support arbitrary numerical settings
basti1302 Jan 31, 2024
36a0681
[paymentservice] support range feature flag for simulated slowness
basti1302 Jan 31, 2024
bb47ccd
[shippingservice] add support for simulated slowness
basti1302 Feb 1, 2024
db9f691
[dash0] add link to additional docs
basti1302 Feb 1, 2024
0746612
[dash0] fix image version
basti1302 Feb 1, 2024
bf6c5a6
[dash0] remove markdownlinkcheck
basti1302 Feb 1, 2024
ab4c4aa
[frontend] update sharp to latest to avoid docker build issue on M1
basti1302 Feb 1, 2024
c2b7549
[dash0] remove pull request template
basti1302 Feb 1, 2024
34e127e
[dash0] deploy new releases automatically
basti1302 Feb 1, 2024
3f97825
[dash0] update .env file automatically after release
basti1302 Feb 1, 2024
581a824
[dash0] bring back arm64 builds via qemu
basti1302 Feb 2, 2024
80bf7d4
[dash0] fix release workflow
basti1302 Feb 2, 2024
fd3b167
[adservice,recommendationservice] add debug logs
basti1302 Feb 2, 2024
6914c0f
[checkoutservice] fix typo in log message
basti1302 Feb 5, 2024
46d4dd1
[frontend] fix typo in ShippingGateway
basti1302 Feb 5, 2024
2cc6a10
[postgres] make init scripts idempotent
basti1302 Feb 5, 2024
213a3f8
[dash0] scripts for deploying the otel demo to k8s locally
basti1302 Feb 5, 2024
6715f2d
[adservice] fix error rate in ad service
basti1302 Feb 14, 2024
b0be0b8
[dash0] fix action to update the .env file
basti1302 Feb 15, 2024
43799a1
[dash0] temporary workflow to update the .env file
basti1302 Feb 15, 2024
c0b2154
[dash0] update image version in .env to 1.1.1 (#15)
github-actions[bot] Feb 15, 2024
7189f02
Revert "[dash0] temporary workflow to update the .env file"
basti1302 Feb 15, 2024
f1b0955
docs(readme): add Dash0 to the list of forks
basti1302 Feb 27, 2024
276c60b
Merge remote-tracking branch 'upstream/main'
basti1302 Feb 27, 2024
61a6d0c
[dash0] fix merge screwup in docker-compose.yml
basti1302 Feb 27, 2024
be042e2
[dash0] make sure to deploy to local Kubernetes context
basti1302 Feb 27, 2024
7b0e629
[dash0] update image version in .env to 1.1.2
github-actions[bot] Feb 27, 2024
73e80f9
[dash0] update .env image version without PR
basti1302 Feb 27, 2024
a9421e5
[dash0] improve local k8s deployment
basti1302 Feb 27, 2024
66f762e
[dash0] chore: remove duplicated utility script
basti1302 Feb 27, 2024
e44d8de
[dash0] update image version in .env to 1.2.0
github-actions[bot] Feb 27, 2024
453a380
[dash0] improve local k8s deplyment
basti1302 Feb 29, 2024
20b9403
[dash0] improve local k8s deployment
basti1302 Mar 11, 2024
0ceb210
[dash0] improve local k8s deployment
basti1302 Mar 11, 2024
87bf8b4
[dash0] improve local k8s deployment
basti1302 Mar 11, 2024
5a45834
[dash0] frontend: add k8s resource detector
basti1302 Mar 13, 2024
1d52ea4
[dash0] frontend: convert k8s resource detector from ts to js
basti1302 Mar 13, 2024
58242b4
[dash0] update image version in .env to 1.2.1
github-actions[bot] Mar 13, 2024
b3ba547
[dash0] frontend: k8s resource detector again
basti1302 Mar 14, 2024
cc29ca5
[dash0] update image version in .env to 1.2.2
github-actions[bot] Mar 14, 2024
838789a
[dash0] chore: improve local k8s deployment
basti1302 Mar 14, 2024
18f92fd
[dash0] chore: improve local k8s deployment
basti1302 Apr 3, 2024
633d021
[dash0] chore: improve local k8s deployment
basti1302 Apr 3, 2024
d740148
[dash0] chore: improve local k8s deployment
basti1302 Apr 3, 2024
d3a49e5
[dash0] docs: remove link to google docs from public README
basti1302 May 16, 2024
14fea03
Added the otellogrus package to integrate logrus with OpenTelemetry (…
marcelbirkner Mar 19, 2025
22df463
[dash0] update image version in .env to 1.2.3
github-actions[bot] Mar 20, 2025
4c7ed92
[dash0] update image version in .env to 1.3.0
github-actions[bot] Mar 20, 2025
80c6f73
Add more useful logging messages
marcelbirkner Mar 20, 2025
a69101d
Update README with release instructions
marcelbirkner Mar 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@


# Demo App version
IMAGE_VERSION=1.8.0
IMAGE_NAME=ghcr.io/open-telemetry/demo
# Images
IMAGE_VERSION=1.3.0
IMAGE_NAME=718306648796.dkr.ecr.eu-west-1.amazonaws.com/opentelemetry-demo
DEMO_VERSION=latest

TRACETEST_IMAGE_VERSION=v0.14.5

# Dependent images
COLLECTOR_CONTRIB_IMAGE=otel/opentelemetry-collector-contrib:0.93.0
GRAFANA_IMAGE=grafana/grafana:10.3.1
30 changes: 0 additions & 30 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

35 changes: 35 additions & 0 deletions .github/actions/deploy-demo/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2024 Dash0 Inc.
# SPDX-License-Identifier: Apache-2.0
name: Deploy Dash0 OTel Demo
description: deploy a release of the Dash0 fork of the OpenTelemetry demo
inputs:
token:
description: 'read/write Github access token for thr dash0-configuration repository'
required: true
containerImageVersion:
description: 'version tag of the Dash0 OTel Demo container images, e.g. 1.1.0'
required: true

runs:
using: "composite"
steps:
- name: checkout dash0-configuration
uses: actions/checkout@v4
with:
repository: dash0hq/dash0-configuration
token: ${{ inputs.token }}
path: dash0-configuration

- name: update container image version
uses: mikefarah/yq@v4.35.2
with:
cmd: yq -i '.otelDemo.containerVersion="${{ inputs.containerImageVersion }}"' dash0-configuration/demo/environments/aws/demo-eu-west-1-demo.yaml
- name: git push dash0-configuration
shell: bash
run: |
cd dash0-configuration
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add demo/environments/aws/demo-eu-west-1-demo.yaml
git commit -m"chore(otel-demo): update otel-demo containerVersion to ${{ inputs.containerImageVersion }}"
git push
35 changes: 35 additions & 0 deletions .github/actions/update-env-file/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2024 Dash0 Inc.
# SPDX-License-Identifier: Apache-2.0
name: Deploy Dash0 OTel Demo
description: deploy a release of the Dash0 fork of the OpenTelemetry demo
inputs:
containerImageVersion:
description: "version tag of the Dash0 OTel Demo container images, e.g. 1.1.0"
required: true
token:
description: "token granting read/write access to dash0-configuration repository."
required: true

runs:
using: "composite"
steps:
- name: check out code
uses: actions/checkout@v4
with:
ref: main
token: ${{ inputs.token }}

- name: update version in .env file
shell: bash
run: |
sed -i "s/^IMAGE_VERSION=[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*$/IMAGE_VERSION=${{ inputs.containerImageVersion }}/" .env
- name: commit and push
shell: bash
run: |
git add .env
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -a -m"[dash0] update image version in .env to ${{ inputs.containerImageVersion }}" -m"[ci skip]"
git status
git push
20 changes: 0 additions & 20 deletions .github/workflows/assign-reviewers.yml

This file was deleted.

39 changes: 22 additions & 17 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

name: Build and Push Images

on:
push:
paths:
@@ -19,6 +21,8 @@ on:
required: false
type: string

workflow_dispatch:

jobs:
build_and_push_images:
runs-on: ubuntu-latest
@@ -28,8 +32,7 @@ jobs:

env:
RELEASE_VERSION: "${{ github.event.release.tag_name }}"
DOCKERHUB_REPO: "otel/demo"
GHCR_REPO: "ghcr.io/open-telemetry/demo"
AWS_ECR_REPO: "718306648796.dkr.ecr.us-west-2.amazonaws.com/opentelemetry-demo"

strategy:
fail-fast: false
@@ -136,30 +139,34 @@ jobs:
echo "Changes detected in ${{ matrix.file_tag.context }}, proceeding with build."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Log in to the Container registry
uses: docker/login-action@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
if: ${{ inputs.push }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
aws-access-key-id: ${{secrets.DEPLOYMENT_WRITE_ECR_ACCESS_KEY}}
aws-secret-access-key: ${{secrets.DEPLOYMENT_WRITE_ECR_SECRET_ACCESS_KEY}}
aws-region: us-west-2
mask-aws-account-id: true

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
if: ${{ inputs.push }}
mask-password: true

- name: Set up QEMU
if: ${{ matrix.file_tag.setup-qemu }}
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:master

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
config-inline: |
[worker.oci]
max-parallelism = 2
- name: Matrix Build and push demo images
if: steps.check_changes.outputs.skip == 'false'
uses: docker/build-push-action@v5.0.0
@@ -169,9 +176,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: ${{ inputs.push }}
tags: |
${{ env.DOCKERHUB_REPO }}:${{ inputs.version }}-${{matrix.file_tag.tag_suffix }}
${{ env.DOCKERHUB_REPO }}:latest-${{matrix.file_tag.tag_suffix }}
${{ env.GHCR_REPO }}:${{ inputs.version }}-${{ matrix.file_tag.tag_suffix }}
${{ env.GHCR_REPO }}:latest-${{ matrix.file_tag.tag_suffix }}
${{ env.AWS_ECR_REPO }}:${{ inputs.version }}-${{ matrix.file_tag.tag_suffix }}
${{ env.AWS_ECR_REPO }}:latest-${{ matrix.file_tag.tag_suffix }}
cache-from: type=gha
cache-to: type=gha
24 changes: 7 additions & 17 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -10,11 +10,6 @@ on:
workflow_dispatch:

jobs:
build_images:
uses: ./.github/workflows/build-images.yml
with:
push: false
version: 'dev'

markdownlint:
runs-on: ubuntu-latest
@@ -53,18 +48,6 @@ jobs:
- name: run misspell
run: make misspell

markdownlinkcheck:
name: markdownlinkcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run link check
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'no'
use-verbose-mode: 'yes'
config-file: '.github/.mlc_config.json'

sanity:
runs-on: ubuntu-latest

@@ -83,3 +66,10 @@ jobs:
run: make install-tools
- name: run checklicense
run: make checklicense

build_images:
uses: ./.github/workflows/build-images.yml
with:
push: false
version: 'dev'
secrets: inherit
39 changes: 38 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
name: "Build and Publish"
name: "Publish Images & Deploy"

on:
release:
@@ -13,3 +13,40 @@ jobs:
push: true
version: ${{ github.event.release.tag_name }}
secrets: inherit

deploy_release:
name: deploy to https://otel-demo.eu-west-1.aws.dash0-demo.com/
needs:
- build_and_push_images
runs-on: ubuntu-latest
# do not run multiple deployment jobs concurrently
concurrency: deploy
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: deploy release
uses: ./.github/actions/deploy-demo
with:
token: ${{ secrets.REPOSITORY_FULL_ACCESS_GITHUB_TOKEN }}
containerImageVersion: ${{ github.event.release.tag_name }}

update-release-in-env-file:
name: update .env file
needs:
- deploy_release
runs-on: ubuntu-latest
concurrency: update-env-file
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: update .env file
uses: ./.github/actions/update-env-file
with:
containerImageVersion: ${{ github.event.release.tag_name }}
token: ${{ secrets.REPOSITORY_FULL_ACCESS_GITHUB_TOKEN }}
23 changes: 0 additions & 23 deletions .github/workflows/stale.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ composer.lock
.venv
.dockerhub.env
.ghcr.env
.aws_ecr.env

src/frontend/cypress/videos
src/frontend/cypress/screenshots
6 changes: 4 additions & 2 deletions .licenserc.json
Original file line number Diff line number Diff line change
@@ -45,6 +45,8 @@
"src/featureflagservice/assets/vendor/",
"src/featureflagservice/priv/",
"src/productcatalogservice/genproto/",
"internal/tools/"
"internal/tools/",
".github/actions/",
"kubernetes/local/"
]
}
}
22 changes: 7 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -76,24 +76,16 @@ install-tools: $(MISSPELL)
build:
docker compose build

.PHONY: build-and-push-dockerhub
build-and-push-dockerhub:
docker compose --env-file .dockerhub.env -f docker-compose.yml build
docker compose --env-file .dockerhub.env -f docker-compose.yml push

.PHONY: build-and-push-ghcr
build-and-push-ghcr:
docker compose --env-file .ghcr.env -f docker-compose.yml build
docker compose --env-file .ghcr.env -f docker-compose.yml push
.PHONY: build-and-push-aws-ecr
build-and-push-aws-ecr:
docker compose --env-file .aws_ecr.env -f docker-compose.yml build
docker compose --env-file .aws_ecr.env -f docker-compose.yml push

.PHONY: build-env-file
build-env-file:
cp .env .dockerhub.env
sed -i '/IMAGE_VERSION=.*/c\IMAGE_VERSION=${RELEASE_VERSION}' .dockerhub.env
sed -i '/IMAGE_NAME=.*/c\IMAGE_NAME=${DOCKERHUB_REPO}' .dockerhub.env
cp .env .ghcr.env
sed -i '/IMAGE_VERSION=.*/c\IMAGE_VERSION=${RELEASE_VERSION}' .ghcr.env
sed -i '/IMAGE_NAME=.*/c\IMAGE_NAME=${GHCR_REPO}' .ghcr.env
cp .env .aws_ecr.env
sed -i '/IMAGE_VERSION=.*/c\IMAGE_VERSION=${RELEASE_VERSION}' .aws_ecr.env
sed -i '/IMAGE_NAME=.*/c\IMAGE_NAME=${AWS_ECR_REPO}' .aws_ecr.env

.PHONY: run-tests
run-tests:
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -8,6 +8,16 @@
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg?color=red)](https://github.com/open-telemetry/opentelemetry-demo/blob/main/LICENSE)
[![Integration Tests](https://github.com/open-telemetry/opentelemetry-demo/actions/workflows/run-integration-tests.yml/badge.svg)](https://github.com/open-telemetry/opentelemetry-demo/actions/workflows/run-integration-tests.yml)

## Dash0

This is Dash0's fork of the OpenTelemetry Demo.

## How to create a new release

1. Create a new tag following the semantic versioning format: `X.Y.Z`. This will trigger a GitHub action to build everything.
2. Create new release using https://github.com/dash0hq/opentelemetry-demo/releases
3. This will automatically update the containerVersion for the demo in ArgoCD

## Welcome to the OpenTelemetry Astronomy Shop Demo

This repository contains the OpenTelemetry Astronomy Shop, a microservice-based
@@ -56,16 +66,16 @@ keeping it up to date for you.

| | | |
|-----------------------------------------|-----------------------------|----------------------------------------------------------------|
| [AlibabaCloud LogService][AlibabaCloud] | [Google Cloud][GoogleCloud] | [Sentry][Sentry] |
| [AppDynamics][AppDynamics] | [Grafana Labs][GrafanaLabs] | [ServiceNow Cloud Observability][ServiceNowCloudObservability] |
| [Aspecto][Aspecto] | [Guance][Guance] | [Splunk][Splunk] |
| [Axiom][Axiom] | [Helios][Helios] | [Sumo Logic][SumoLogic] |
| [Axoflow][Axoflow] | [Honeycomb.io][Honeycombio] | [TelemetryHub][TelemetryHub] |
| [Azure Data Explorer][Azure] | [Instana][Instana] | [Teletrace][Teletrace] |
| [Coralogix][Coralogix] | [Kloudfuse][Kloudfuse] | [Tracetest][Tracetest] |
| [AlibabaCloud LogService][AlibabaCloud] | [Elastic][Elastic] | [OpenSearch][OpenSearch] |
| [AppDynamics][AppDynamics] | [Google Cloud][GoogleCloud] | [Sentry][Sentry] |
| [Aspecto][Aspecto] | [Grafana Labs][GrafanaLabs] | [ServiceNow Cloud Observability][ServiceNowCloudObservability] |
| [Axiom][Axiom] | [Guance][Guance] | [Splunk][Splunk] |
| [Axoflow][Axoflow] | [Helios][Helios] | [Sumo Logic][SumoLogic] |
| [Azure Data Explorer][Azure] | [Honeycomb.io][Honeycombio] | [TelemetryHub][TelemetryHub] |
| [Coralogix][Coralogix] | [Instana][Instana] | [Teletrace][Teletrace] |
| [Dash0][Dash0] | [Kloudfuse][Kloudfuse] | [Tracetest][Tracetest] |
| [Datadog][Datadog] | [Logz.io][Logzio] | [Uptrace][Uptrace] |
| [Dynatrace][Dynatrace] | [New Relic][NewRelic] | |
| [Elastic][Elastic] | [OpenSearch][OpenSearch] | |

## Contributing

@@ -112,6 +122,7 @@ Emeritus:
[Axoflow]: https://axoflow.com/opentelemetry-support-in-more-detail-in-axosyslog-and-syslog-ng/
[Azure]: https://github.com/Azure/Azure-kusto-opentelemetry-demo
[Coralogix]: https://coralogix.com/blog/configure-otel-demo-send-telemetry-data-coralogix
[Dash0]: https://github.com/dash0hq/opentelemetry-demo
[Datadog]: https://github.com/DataDog/opentelemetry-demo
[Dynatrace]: https://www.dynatrace.com/news/blog/opentelemetry-demo-application-with-dynatrace/
[Elastic]: https://github.com/elastic/opentelemetry-demo
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -284,6 +284,7 @@ services:
- CART_SERVICE_ADDR
- CHECKOUT_SERVICE_ADDR
- CURRENCY_SERVICE_ADDR
- FEATURE_FLAG_GRPC_SERVICE_ADDR
- PRODUCT_CATALOG_SERVICE_ADDR
- RECOMMENDATION_SERVICE_ADDR
- SHIPPING_SERVICE_ADDR
@@ -413,6 +414,7 @@ services:
- OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE
- OTEL_RESOURCE_ATTRIBUTES
- OTEL_SERVICE_NAME=paymentservice
- FEATURE_FLAG_GRPC_SERVICE_ADDR
depends_on:
otelcol:
condition: service_started
@@ -528,6 +530,7 @@ services:
environment:
- SHIPPING_SERVICE_PORT
- QUOTE_SERVICE_ADDR
- FEATURE_FLAG_GRPC_SERVICE_ADDR
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://${OTEL_COLLECTOR_HOST}:${OTEL_COLLECTOR_PORT_GRPC}/v1/traces
- OTEL_RESOURCE_ATTRIBUTES
- OTEL_SERVICE_NAME=shippingservice
1 change: 1 addition & 0 deletions kubernetes/local/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dash0-otel-demo-local-k8s-values.yaml
4 changes: 4 additions & 0 deletions kubernetes/local/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Helper Scripts to Deploy the Dash0 fork of the OpenTelemetry Demo to Kubernetes Locally
=======================================================================================

See <https://docs.google.com/document/d/1ASCn8AUR0ehqSynsUkTqvH2biTYsFAxz1rXi1Lkh3DE/edit#heading=h.sku3rt6c70ac>.
39 changes: 39 additions & 0 deletions kubernetes/local/create-values-yaml.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash

set -euo pipefail

cd -P -- "$(dirname -- "$0")"

source utils

if [[ -n ${VALUES_YAML:-} ]]; then
echo "Using custom values yaml file: $VALUES_YAML"
cp $VALUES_YAML dash0-otel-demo-local-k8s-values.yaml
else
echo "Creating dash0-otel-demo-local-k8s-values.yaml based on:"
echo "- $dash0_configuration_dir/demo/environments/aws/demo-eu-west-1-demo.yaml and"
echo "- $dash0_configuration_dir/demo/values.yaml"
echo "Set VALUES_YAML to use a custom values file."
echo
yq \
". *= load(\"$dash0_configuration_dir/demo/environments/aws/demo-eu-west-1-demo.yaml\")" \
$dash0_configuration_dir/demo/values.yaml | \
yq --from-file dash0-otel-demo-local-k8s.yq > \
dash0-otel-demo-local-k8s-values.yaml

if [[ -n ${WITH_RECORDER:-} ]]; then
echo "Adding an additional exporter to send to the recorder (needs to be deployed separately)"
yq -i --from-file with-export-to-recorder.yq dash0-otel-demo-local-k8s-values.yaml
fi

if [[ -n ${OTEL_EXPORTER_OTLP_ENDPOINT:-} ]]; then
echo "Using non-default reporting endpoint from environment variable"
echo "OTEL_EXPORTER_OTLP_ENDPOINT: $OTEL_EXPORTER_OTLP_ENDPOINT"
echo
yq -i ".opentelemetry-collector.config.exporters.otlp/dash0-dev.endpoint=\"$OTEL_EXPORTER_OTLP_ENDPOINT\" | del(.opentelemetry-collector.config.exporters.otlp/dash0-dev.auth) " dash0-otel-demo-local-k8s-values.yaml
else
echo "Reporting to $(yq '.opentelemetry-collector.config.exporters["otlp/dash0-dev"]'.endpoint dash0-otel-demo-local-k8s-values.yaml), set OTEL_EXPORTER_OTLP_ENDPOINT to report somewhere else."
echo
fi
fi

17 changes: 17 additions & 0 deletions kubernetes/local/dash0-otel-demo-local-k8s.yq
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.otelDemo.helm |
del(.grafana.*) |
.grafana.enabled=false |
del(.jaeger.*) |
.jaeger.enabled=false |
del(.opentelemetry-collector.config.exporters.otlp) |
del(.prometheus.*) |
.prometheus.enabled=false |
del(.opensearch.*) |
.opensearch.enabled=false |
.default.image.pullSecrets=[{"name": "regcred"}] |
del(.opentelemetry-collector.config.exporters.prometheus) |
del(.opentelemetry-collector.config.exporters.otlp/dash0-prod) |
del(.opentelemetry-collector.config.exporters.otlp/dash0-prod-pen-testing) |
del(.opentelemetry-collector.config.service.pipelines.*.exporters.[] | select(. == "otlp/dash0-prod*") ) |
del(.opentelemetry-collector.config.service.pipelines.traces.exporters.[] | select(. == "otlp") ) |
.opentelemetry-collector.config.processors.resource.attributes[] |= select(.key == "cloud.account.id").value |= "012345678901"
37 changes: 37 additions & 0 deletions kubernetes/local/deploy-recorder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.

set -euo pipefail

cd -P -- "$(dirname -- "$0")"

source utils

if [[ ${1:-} != "no-context-switch" ]]; then
# If called directly from the shell (and not from deploy.sh etc.), we need to
# make sure that we work in the local Kubernetes context.
trap switch_back_to_original_context EXIT
switch_to_local_context
fi

refresh_image_pull_secret otel-demo

# remove previous deployment, if it exists
./teardown-recorder.sh no-context-switch

sleep 5

LOCAL_DATA_DIR=$HOME/data
mkdir -p $LOCAL_DATA_DIR

yq -i ".extraVolumes[0].hostPath.path=\"$LOCAL_DATA_DIR\"" deploy-recorder.yaml

echo "deploying recorder"
helm install \
--namespace otel-demo \
--create-namespace \
dash0-load-test-recorder \
open-telemetry/opentelemetry-collector \
--values deploy-recorder.yaml

86 changes: 86 additions & 0 deletions kubernetes/local/deploy-recorder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
nameOverride: load-test-recorder
mode: deployment
ports:
# Explicitly disable the default ports
jaeger-compact:
enabled: false
jaeger-thrift:
enabled: false
jaeger-grpc:
enabled: false
zipkin:
enabled: false
# Expose the self-monitoring Prometheus metric scrape endpoint
# metrics:
# enabled: true
replicaCount: 1
podDisruptionBudget:
enabled: true
minAvailable: 1
autoscaling:
enabled: true
resources:
requests:
cpu: 256m
memory: 512Mi
limits:
cpu: 4
memory: 8Gi
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/scheme: "http"
prometheus.io/path: "/metrics"
prometheus.io/port: "8888"
extraVolumes:
- name: recorded-data
hostPath:
path: /will/be/replaced/by/deploy-recorder.sh
type: Directory
extraVolumeMounts:
- name: recorded-data
mountPath: /data
config:
extensions:
# The health_check extension is mandatory for this chart.
# Without the health_check extension the collector will fail the readiness and liveliness probes.
# The health_check extension can be modified, but should never be removed.
health_check: {}
memory_ballast:
size_in_percentage: "0"
size_mib: 0
receivers:
# Disable the standard Jaeger, Zipkin & Prometheus receivers.
jaeger: null
zipkin: null
prometheus: null
exporters:
debug: {}
# verbosity: detailed
file/metrics:
path: /data/metrics.json
file/traces:
path: /data/traces.json
file/logs:
path: /data/logs.json
service:
extensions:
- health_check
pipelines:
metrics:
receivers:
- otlp
exporters:
- debug
- file/metrics
traces:
receivers:
- otlp
exporters:
- debug
- file/traces
logs:
receivers:
- otlp
exporters:
- debug
- file/logs
52 changes: 52 additions & 0 deletions kubernetes/local/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash

set -euo pipefail

cd -P -- "$(dirname -- "$0")"

source utils

git_pull_dash0_configuration

trap switch_back_to_original_context EXIT
switch_to_local_context

refresh_image_pull_secret otel-demo

WITH_RECORDER="" ./teardown.sh no-context-switch

if [[ -n ${WITH_RECORDER:-} ]]; then
./deploy-recorder.sh no-context-switch
fi

sleep 5

./create-values-yaml.sh $dash0_configuration_dir

echo Deploying PostgreSQL
helm install --namespace otel-demo --create-namespace opentelemetry-demo-postgresql oci://registry-1.docker.io/bitnamicharts/postgresql --values postgres-values.yaml
echo Deploying OpenTelemetry Demo
helm install \
--namespace otel-demo \
--create-namespace \
opentelemetry-demo \
open-telemetry/opentelemetry-demo \
--values dash0-otel-demo-local-k8s-values.yaml
echo Deploying PostgreSQL Service
kubectl apply --namespace otel-demo -f postgres-service.yaml

echo Copying PostgreSQL Init Files
# TODO we would actually need to wait until the postgresql container is up, this might take a while in case it is pulling a new image.
sleep 3
kubectl cp ../../src/ffspostgres/init-scripts/10-ffs_schema.sql --namespace otel-demo opentelemetry-demo-postgresql-0:/tmp/
kubectl cp ../../src/ffspostgres/init-scripts/20-ffs_data.sql --namespace otel-demo opentelemetry-demo-postgresql-0:/tmp/
echo Running PostgreSQL Init Files
kubectl exec --namespace otel-demo opentelemetry-demo-postgresql-0 -- psql postgresql://ffs:ffs@localhost/ffs -a -f /tmp/10-ffs_schema.sql
kubectl exec --namespace otel-demo opentelemetry-demo-postgresql-0 -- psql postgresql://ffs:ffs@localhost/ffs -a -f /tmp/20-ffs_data.sql

echo waiting for the frontend and frontendproxy pod to become ready
sleep 5
kubectl wait --namespace otel-demo --for=condition=ready pod -l app.kubernetes.io/component=frontendproxy --timeout 10s
kubectl wait --namespace otel-demo --for=condition=ready pod -l app.kubernetes.io/component=frontend --timeout 20s
./port-forward.sh

6 changes: 6 additions & 0 deletions kubernetes/local/port-forward.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

set -euo pipefail

kubectl port-forward --namespace otel-demo service/opentelemetry-demo-frontendproxy 8080:8080

15 changes: 15 additions & 0 deletions kubernetes/local/postgres-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: opentelemetry-demo-ffspostgres
spec:
ports:
- name: tcp-postgresql
port: 5432
protocol: TCP
targetPort: tcp-postgresql
selector:
app.kubernetes.io/component: primary
app.kubernetes.io/instance: opentelemetry-demo-postgresql
app.kubernetes.io/name: postgresql
type: ClusterIP
6 changes: 6 additions & 0 deletions kubernetes/local/postgres-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
global:
postgresql:
auth:
username: "ffs"
password: "ffs"
database: "ffs"
18 changes: 18 additions & 0 deletions kubernetes/local/teardown-recorder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

set -euo pipefail

cd -P -- "$(dirname -- "$0")"

source utils

if [[ ${1:-} != "no-context-switch" ]]; then
# If called directly from the shell (and not from deploy.sh etc.), we need to
# make sure that we work in the local Kubernetes context.
trap switch_back_to_original_context EXIT
switch_to_local_context
fi

echo "removing recorder"
helm uninstall --namespace otel-demo --ignore-not-found dash0-load-test-recorder

27 changes: 27 additions & 0 deletions kubernetes/local/teardown.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

set -euo pipefail

cd -P -- "$(dirname -- "$0")"

source utils

if [[ ${1:-} != "no-context-switch" ]]; then
# If called directly from the shell (and not from deploy.sh etc.), we need to
# make sure that we work in the local Kubernetes context.
trap switch_back_to_original_context EXIT
switch_to_local_context
fi

# tear down things installed by deploy.sh
echo Removing PostgreSQL
helm uninstall --namespace otel-demo --ignore-not-found opentelemetry-demo-postgresql
echo Removing OpenTelemetry Demo
helm uninstall --namespace otel-demo --ignore-not-found opentelemetry-demo
echo Removing PostgreSQL Service
kubectl delete --namespace otel-demo --ignore-not-found -f postgres-service.yaml

if [[ -n ${WITH_RECORDER:-} ]]; then
./teardown-recorder.sh no-context-switch
fi

55 changes: 55 additions & 0 deletions kubernetes/local/utils
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
dash0_configuration_dir=../../../dash0-configuration

# Switches to the local Kubernetes context (docker-desktop by default) and sets
# the variable original_context to the previously active context name.
function switch_to_local_context {
original_context=$(kubectx --current)
local_context=docker-desktop
if [[ -n ${LOCAL_KUBERNETES_CONTEXT:-} ]]; then
local_context=$LOCAL_KUBERNETES_CONTEXT
fi
echo "switching to Kubernetes context $local_context"
kubectx $local_context
}

# Switches back to the orignal local Kubernetes context that was active before
# switch_to_local_context had been called.
function switch_back_to_original_context {
if [[ -z $original_context ]]; then
echo "Error: switch_back_to_original_context requires that switch_to_local_context has been called earlier."
exit 1
fi
echo "switching back to Kubernetes context $original_context"
kubectx $original_context
}

function git_pull_dash0_configuration {
if [[ ! -e $dash0_configuration_dir ]]; then
echo Error: expected $dash0_configuration_dir to exist, but it does not.
exit 1
fi
echo executing git pull in $dash0_configuration_dir
pushd $dash0_configuration_dir > /dev/null
git pull
popd > /dev/null
}

function refresh_image_pull_secret {
echo
echo "Note: You might need to execute \"aws sso login\" before trying to deploy."
echo
namespaces=("$@")
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 718306648796.dkr.ecr.eu-west-1.amazonaws.com
for n in "${namespaces[@]}"; do
if kubectl get namespaces --show-labels -l kubernetes.io/metadata.name=$n | grep $n; then
echo "namespace $n exists"
else
echo "creating namespace $n"
kubectl create namespace $n
fi
echo "recreating image pull secret in namespace $n"
kubectl delete secret --namespace=$n --ignore-not-found regcred
kubectl create secret docker-registry regcred --namespace=$n --docker-server=718306648796.dkr.ecr.eu-west-1.amazonaws.com --docker-username=AWS --docker-password=$(aws ecr get-login-password --region eu-west-1 --profile Demo_Dash0DeveloperAssumeRole)
done
}

2 changes: 2 additions & 0 deletions kubernetes/local/with-export-to-recorder.yq
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.opentelemetry-collector.config.exporters.otlp/recorder={"endpoint": "dash0-load-test-recorder:4317", "tls": {"insecure": true}} |
.opentelemetry-collector.config.service.pipelines.[].exporters += "otlp/recorder"
803 changes: 246 additions & 557 deletions kubernetes/opentelemetry-demo.yaml

Large diffs are not rendered by default.

78 changes: 63 additions & 15 deletions pb/demo.proto
Original file line number Diff line number Diff line change
@@ -264,48 +264,96 @@ message Ad {
// ------------Feature flag service------------------

service FeatureFlagService {
rpc GetFlag(GetFlagRequest) returns (GetFlagResponse) {}

/* Returns the raw value of the feature flag. If no flag with that name exists, the response will have the value 0. */
rpc GetFeatureFlagValue(GetFeatureFlagValueRequest) returns (GetFeatureFlagValueResponse) {}

/*
* A convenience function to evaluate a feature flag that represents a probability, that is, a flag with a value between 0 and 1
* Returns the random decision as a boolean value. Services that use probability based feature flags can use this method instead
* of implementing the random decision taking on their own. If no flag with that name exists, the response will have the
* value false.
*/
rpc EvaluateProbabilityFeatureFlag(EvaluateProbabilityFeatureFlagRequest) returns (EvaluateProbabilityFeatureFlagResponse) {}

/*
* A convenience function to fetch a triplet of feature flag values that represent a range of values. For example, for
* simulating increased request latencies in a service, the triplet of feature flags to be created would be
* - serviceNameSimulateSlowness: on/off switch or probability for the feature (value is >= 0 & <= 1),
* - serviceNameSimulateSlownessLowerBound: the lower bound in milliseconds for the additional delay,
* - serviceNameSimulateSlownessUpperBound: the upper bound in milliseconds for the additional delay.
* That is, the feature flag API assumes that there are additional feature flag values with the suffix "LowerBound" and
* "UpperBound" when a range feature flag with a particular name is requested. In case the feature is enabled, but one or both
* of lower/upper bound does not exist, the arbitrary defaults for the lower and upper bound are 0 and 1000 respectively.
*/
rpc GetRangeFeatureFlag(RangeFeatureFlagRequest) returns (RangeFeatureFlagResponse) {}

/* Creates a new feature flag. */
rpc CreateFlag(CreateFlagRequest) returns (CreateFlagResponse) {}
rpc UpdateFlag(UpdateFlagRequest) returns (UpdateFlagResponse) {}

/* Updates the value of a feature flag. */
rpc UpdateFlagValue(UpdateFlagValueRequest) returns (UpdateFlagValueResponse) {}

/* Lists all feature flags with their descriptions and values. */
rpc ListFlags(ListFlagsRequest) returns (ListFlagsResponse) {}

/* Deletes an existing feature flag. */
rpc DeleteFlag(DeleteFlagRequest) returns (DeleteFlagResponse) {}
}

message Flag {
message FlagDefinition {
string name = 1;
string description = 2;
bool enabled = 3;
float value = 3;
}

message GetFlagRequest {
message EvaluateProbabilityFeatureFlagRequest {
string name = 1;
}

message GetFlagResponse {
Flag flag = 1;
message EvaluateProbabilityFeatureFlagResponse {
bool enabled = 1;
}

message RangeFeatureFlagRequest {
string name = 1;
string nameLowerBound = 2;
string nameUpperBound = 3;
}

message RangeFeatureFlagResponse {
bool enabled = 1;
float lowerBound = 2;
float upperBound = 3;
}

message GetFeatureFlagValueRequest {
string name = 1;
}

message GetFeatureFlagValueResponse {
float value = 3;
}

message CreateFlagRequest {
string name = 1;
string description = 2;
bool enabled = 3;
float value = 3;
}

message CreateFlagResponse {
Flag flag = 1;
}
message CreateFlagResponse {}

message UpdateFlagRequest {
message UpdateFlagValueRequest {
string name = 1;
bool enabled = 2;
float value = 2;
}

message UpdateFlagResponse {}
message UpdateFlagValueResponse {}

message ListFlagsRequest {}

message ListFlagsResponse {
repeated Flag flag = 1;
repeated FlagDefinition flag = 1;
}

message DeleteFlagRequest {
959 changes: 632 additions & 327 deletions src/accountingservice/genproto/oteldemo/demo.pb.go

Large diffs are not rendered by default.

176 changes: 142 additions & 34 deletions src/accountingservice/genproto/oteldemo/demo_grpc.pb.go

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions src/adservice/src/main/java/oteldemo/AdService.java
Original file line number Diff line number Diff line change
@@ -34,7 +34,8 @@
import oteldemo.Demo.Ad;
import oteldemo.Demo.AdRequest;
import oteldemo.Demo.AdResponse;
import oteldemo.Demo.GetFlagResponse;
import oteldemo.Demo.EvaluateProbabilityFeatureFlagRequest;
import oteldemo.Demo.EvaluateProbabilityFeatureFlagResponse;
import oteldemo.FeatureFlagServiceGrpc.FeatureFlagServiceBlockingStub;

public final class AdService {
@@ -137,6 +138,7 @@ private AdServiceImpl(FeatureFlagServiceBlockingStub featureFlagServiceStub) {
*/
@Override
public void getAds(AdRequest req, StreamObserver<AdResponse> responseObserver) {
logger.debug("received getAds request");
AdService service = AdService.getInstance();

// get the current span in context
@@ -176,14 +178,16 @@ public void getAds(AdRequest req, StreamObserver<AdResponse> responseObserver) {
Attributes.of(
adRequestTypeKey, adRequestType.name(), adResponseTypeKey, adResponseType.name()));

logger.debug("checking adServiceFailure feature flag");
if (checkAdFailure()) {
logger.warn(ADSERVICE_FAIL_FEATURE_FLAG + " fail feature flag enabled");
logger.warn(ADSERVICE_FAIL_FEATURE_FLAG + " fail feature flag enabled, failing request.");
throw new StatusRuntimeException(Status.RESOURCE_EXHAUSTED);
}

AdResponse reply = AdResponse.newBuilder().addAllAds(allAds).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
logger.debug("getAds request completed");
} catch (StatusRuntimeException e) {
span.addEvent(
"Error", Attributes.of(AttributeKey.stringKey("exception.message"), e.getMessage()));
@@ -198,17 +202,12 @@ boolean checkAdFailure() {
return false;
}

// Flip a coin and fail 1/10th of the time if feature flag is enabled
if (random.nextInt(10) != 1) {
return false;
}

GetFlagResponse response =
featureFlagServiceStub.getFlag(
oteldemo.Demo.GetFlagRequest.newBuilder()
EvaluateProbabilityFeatureFlagResponse response =
featureFlagServiceStub.evaluateProbabilityFeatureFlag(
EvaluateProbabilityFeatureFlagRequest.newBuilder()
.setName(ADSERVICE_FAIL_FEATURE_FLAG)
.build());
return response.getFlag().getEnabled();
return response.getEnabled();
}
}

5 changes: 3 additions & 2 deletions src/adservice/src/main/resources/log4j2.xml
Original file line number Diff line number Diff line change
@@ -18,14 +18,15 @@ limitations under the License.
<Configuration status="WARN">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %n"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} - %level{WARN=WARNING, DEBUG=DEBUG, ERROR=ERROR, TRACE=TRACE, INFO=INFO} - %logger{36} - %msg trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="io.grpc.netty" level="INFO"/>
<Logger name="io.netty" level="INFO"/>
<Logger name="sun.net" level="INFO"/>
<Root level="INFO">
<Logger name="oteldemo" level="DEBUG"/>
<Root level="DEBUG">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
6 changes: 3 additions & 3 deletions src/cartservice/src/featureflags/FeatureFlagHelper.cs
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ public async Task<bool> GenerateCartError()
return false;
}

var getFlagRequest = new GetFlagRequest { Name = "cartServiceFailure" };
var getFlagResponse = await _featureFlagServiceClient.GetFlagAsync(getFlagRequest);
return getFlagResponse.Flag.Enabled;
var featureFlagRequest = new EvaluateProbabilityFeatureFlagRequest { Name = "cartServiceFailure" };
var featureFlagResponse = await _featureFlagServiceClient.EvaluateProbabilityFeatureFlagAsync(featureFlagRequest);
return featureFlagResponse.Enabled;
}
}
959 changes: 632 additions & 327 deletions src/checkoutservice/genproto/oteldemo/demo.pb.go

Large diffs are not rendered by default.

176 changes: 142 additions & 34 deletions src/checkoutservice/genproto/oteldemo/demo_grpc.pb.go

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions src/featureflagservice/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Feature Flag Service

This project provides an web interface for creating and updating feature flags
and a GRPC service for fetching the status of flags by their name. Each runs on
their own port but are in the same Release.
This project provides a web interface and a GRPC API for creating, updating and
fetching settings and feature flags. The web interface and the GRPC API each run
on their own port but are in the same Release.

Historically, the service only handled simple boolean feature flags, but it now
has evolved to handling arbitrary numerical settings. Feature flags that have a
simple on/off semantic can be represented with the values 1 and 0 respectively.
Feature with a probability semantic (that is, for each request there is a random
decision whether the flag is active or not), values between 0.0 and 1.0 can be
used.

## Running

Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ defmodule Featureflagservice.FeatureFlags.FeatureFlag do
feature_flag
|> cast(attrs, [:name, :description, :enabled])
|> validate_required([:name, :description, :enabled])
|> validate_number(:enabled, greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0)
|> unique_constraint(:name)
end
end
Original file line number Diff line number Diff line change
@@ -29,14 +29,15 @@
<%= text_input f, :description %>
<%= error_tag f, :description %>

<%= label f, :enabled %>
<%= number_input f, :enabled, min: 0, max: 1, step: 0.01, "aria-describedby": "enabled_help_text" %>
<p id="enabled_help_text" style="font-size: smaller; margin-top: -10px;">
A decimal value between 0 and 1 (inclusive)<br />
0.0 is always disabled<br />
1.0 is always enabled<br />
All values between set a percentage chance on each request<br />
example: 0.55 is enabled 55% of the time<br />
<label for="feature_flag_enabled">Value</label>
<%= number_input f, :enabled, step: 0.01, "aria-describedby": "value_help_text" %>
<p id="value_help_text" style="font-size: smaller; margin-top: -10px;">
A decimal value. Most values are interpreted as a <em>probability</em>, feature flags of that kind
should have a value between 0.0 (inclusive) and 1.0 (inclusive). A feature flag with the value 0.0 is
always disabled, a flag with the value 1.0 is always enabled. Values between 0 and 1 define the
probability for the feature being active for a given request.<br />
Ultimtately though, feature flags can have arbitrary numeric values and it is up to the service
reading the feature flag setting to interprete it.
</p>
<%= error_tag f, :enabled %>

Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
<tr>
<th>Name</th>
<th>Description</th>
<th>Enabled</th>
<th>Value</th>

<th></th>
</tr>
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@
</li>

<li>
<strong>Enabled:</strong>
<strong>Value:</strong>
<%= @feature_flag.enabled %>
</li>

Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
<tr>
<th>Name</th>
<th>Description</th>
<th>Enabled</th>
<th>Value</th>

<th></th>
</tr>
194 changes: 166 additions & 28 deletions src/featureflagservice/src/ffs_service.erl
Original file line number Diff line number Diff line change
@@ -16,57 +16,195 @@

-behaviour(ffs_service_bhvr).

-export([get_flag/2,
-export([get_feature_flag_value/2,
evaluate_probability_feature_flag/2,
get_range_feature_flag/2,
create_flag/2,
update_flag/2,
update_flag_value/2,
list_flags/2,
delete_flag/2]).

-include_lib("grpcbox/include/grpcbox.hrl").

-include_lib("opentelemetry_api/include/otel_tracer.hrl").

-spec get_flag(ctx:t(), ffs_demo_pb:get_flag_request()) ->
{ok, ffs_demo_pb:get_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
get_flag(Ctx, #{name := Name}) ->
-spec get_feature_flag_value(ctx:t(), ffs_demo_pb:get_feature_flag_value_request()) ->
{ok, ffs_demo_pb:get_feature_flag_value_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
get_feature_flag_value(Ctx, #{name := Name}) ->
case 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(Name) of
nil ->
{grpc_error, {?GRPC_STATUS_NOT_FOUND, <<"the requested feature flag does not exist">>}};
#{'__struct__' := 'Elixir.Featureflagservice.FeatureFlags.FeatureFlag',
description := Description,
enabled := Enabled
} ->
RandomNumber = rand:uniform(100), % Generate a random number between 0 and 100
Probability = trunc(Enabled * 100), % Convert the Enabled value to a percentage
FlagEnabledValue = RandomNumber =< Probability, % Determine if the random number falls within the probability range
% Do not fail with a GRPC error when feature flag has not been configured, instead just return 0.
% This allows services to seamlessly introduce new feature flags without requiring that every
% deployment of the demo immediately sets them.
{ok, #{value => 0.0}, Ctx};

#{ enabled := Value } ->

?set_attribute('app.featureflag.name', Name),
?set_attribute('app.featureflag.raw_value', Enabled),
?set_attribute('app.featureflag.raw_value', Value),

{ok, #{value => Value}, Ctx};

_ ->
{grpc_error, {?GRPC_STATUS_INTERNAL, <<"unexpected response from get_feature_flag_by_name">>}}

end.

-spec evaluate_probability_feature_flag(ctx:t(), ffs_demo_pb:evaluate_probability_feature_flag_request()) ->
{ok, ffs_demo_pb:evaluate_probability_feature_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
evaluate_probability_feature_flag(Ctx, #{name := Name}) ->
case 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(Name) of
nil ->
% Do not fail with a GRPC error when feature flag has not been configured, instead just return false.
% This allows services to seamlessly introduce new feature flags without requiring that every
% deployment of the demo immediately sets them.
{ok, #{enabled => false}, Ctx};

#{ enabled := EnabledRaw } ->
RandomNumber = rand:uniform(),
FlagEnabledValue = RandomNumber =< EnabledRaw,

?set_attribute('app.featureflag.name', Name),
?set_attribute('app.featureflag.raw_value', EnabledRaw),
?set_attribute('app.featureflag.enabled', FlagEnabledValue),

Flag = #{name => Name,
description => Description,
enabled => FlagEnabledValue},
{ok, #{enabled => FlagEnabledValue}, Ctx};

_ ->
{grpc_error, {?GRPC_STATUS_INTERNAL, <<"unexpected response from get_feature_flag_by_name">>}}

end.

-spec get_range_feature_flag(ctx:t(), ffs_demo_pb:get_range_feature_flag_request()) ->
{ok, ffs_demo_pb:get_range_feature_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
get_range_feature_flag(Ctx, #{name := Name, nameLowerBound := NameLowerBound, nameUpperBound := NameUpperBound }) ->
EnabledFlag = 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(Name),
case EnabledFlag of
nil ->
% Do not fail with a GRPC error when feature flag has not been configured, instead just return false.
% This allows services to seamlessly introduce new feature flags without requiring that every
% deployment of the demo immediately sets them.
{ok, #{enabled => false, lowerBound => 0, upperBound => 0}, Ctx};

#{ enabled := EnabledRaw } ->
RandomNumber = rand:uniform(),
FlagEnabledValue = RandomNumber =< EnabledRaw,

?set_attribute('app.featureflag.name', Name),
?set_attribute('app.featureflag.raw_value', EnabledRaw),
?set_attribute('app.featureflag.enabled', FlagEnabledValue),

if
FlagEnabledValue ->
LowerBound = 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(NameLowerBound),
UpperBound = 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(NameUpperBound),
LowerBoundDefault = 0,
UpperBoundDefault = 1000,
case {LowerBound, UpperBound} of
{#{ enabled := RawValueLowerBound }, #{ enabled := RawValueUpperBound }} ->
{ok, #{enabled => FlagEnabledValue, lowerBound => RawValueLowerBound, upperBound => RawValueUpperBound}, Ctx};
{#{ enabled := RawValueLowerBound }, _} ->
{ok, #{enabled => FlagEnabledValue, lowerBound => RawValueLowerBound, upperBound => UpperBoundDefault}, Ctx};
{_, #{ enabled := RawValueUpperBound }} ->
{ok, #{enabled => FlagEnabledValue, lowerBound => LowerBoundDefault, upperBound => RawValueUpperBound}, Ctx};
_ ->
{ok, #{enabled => FlagEnabledValue, lowerBound => LowerBoundDefault, upperBound => UpperBoundDefault}, Ctx}
end;

true ->
% flag is not enabled
{ok, #{enabled => false, lowerBound => 0, upperBound => 0}, Ctx}
end;

_ ->
{grpc_error, {?GRPC_STATUS_INTERNAL, <<"unexpected response from get_feature_flag_by_name">>}}

{ok, #{flag => Flag}, Ctx}
end.

-spec create_flag(ctx:t(), ffs_demo_pb:create_flag_request()) ->
{ok, ffs_demo_pb:create_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
create_flag(_Ctx, _) ->
{grpc_error, {?GRPC_STATUS_UNIMPLEMENTED, <<"use the web interface to create flags.">>}}.
create_flag(Ctx, Flag) ->
case Flag of
nil ->
{grpc_error, {?GRPC_STATUS_INVALID_ARGUMENT, <<"Flag is nil">>}};

-spec update_flag(ctx:t(), ffs_demo_pb:update_flag_request()) ->
{ok, ffs_demo_pb:update_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
update_flag(_Ctx, _) ->
{grpc_error, {?GRPC_STATUS_UNIMPLEMENTED, <<"use the web interface to update flags.">>}}.
#{
name := Name,
description := Description,
value := Value
} ->
'Elixir.Featureflagservice.FeatureFlags':create_feature_flag(#{
name => Name,
description => Description,
enabled => Value
}),

?set_attribute('app.featureflag.name', Name),
?set_attribute('app.featureflag.raw_value', Value),

{ok, #{}, Ctx};

_ ->
{grpc_error, {?GRPC_STATUS_INVALID_ARGUMENT, <<"Malformed flag definition">>}}

end.

-spec update_flag_value(ctx:t(), ffs_demo_pb:update_flag_value_request()) ->
{ok, ffs_demo_pb:update_flag_value_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
update_flag_value(Ctx, #{name := Name, value := Value}) ->
Flag = 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(Name),
case Flag of
nil ->
{grpc_error, {?GRPC_STATUS_NOT_FOUND, <<"the requested feature flag does not exist">>}};

#{'__struct__' := 'Elixir.Featureflagservice.FeatureFlags.FeatureFlag'} ->
'Elixir.Featureflagservice.FeatureFlags':update_feature_flag(
Flag,
#{enabled => Value}
),

?set_attribute('app.featureflag.name', Name),
?set_attribute('app.featureflag.raw_value', Value),

{ok, #{}, Ctx};

_ ->
{grpc_error, {?GRPC_STATUS_INTERNAL, <<"unexpected response from get_feature_flag_by_name">>}}
end.

-spec list_flags(ctx:t(), ffs_demo_pb:list_flags_request()) ->
{ok, ffs_demo_pb:list_flags_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
list_flags(_Ctx, _) ->
{grpc_error, {?GRPC_STATUS_UNIMPLEMENTED, <<"use the web interface to view all flags.">>}}.
list_flags(Ctx, _) ->
Flags = lists:map(fun unpack_flag/1, 'Elixir.Featureflagservice.FeatureFlags':list_feature_flags()),
{ok, #{flag => Flags}, Ctx}.

unpack_flag(Flag) ->
case Flag of
#{
name := Name,
description := Description,
enabled := Value
} ->
#{name => Name,
description => Description,
value => Value}
end.

-spec delete_flag(ctx:t(), ffs_demo_pb:delete_flag_request()) ->
{ok, ffs_demo_pb:delete_flag_response(), ctx:t()} | grpcbox_stream:grpc_error_response().
delete_flag(_Ctx, _) ->
{grpc_error, {?GRPC_STATUS_UNIMPLEMENTED, <<"use the web interface to delete flags.">>}}.
delete_flag(Ctx, #{name := Name}) ->
Flag = 'Elixir.Featureflagservice.FeatureFlags':get_feature_flag_by_name(Name),
case Flag of
nil ->
{grpc_error, {?GRPC_STATUS_NOT_FOUND, <<"the requested feature flag does not exist">>}};

#{'__struct__' := 'Elixir.Featureflagservice.FeatureFlags.FeatureFlag'} ->
'Elixir.Featureflagservice.FeatureFlags':delete_feature_flag(Flag),

?set_attribute('app.featureflag.name', Name),

{ok, #{}, Ctx};

_ ->
{grpc_error, {?GRPC_STATUS_INTERNAL, <<"unexpected response from get_feature_flag_by_name">>}}
end.
5 changes: 3 additions & 2 deletions src/ffspostgres/init-scripts/10-ffs_schema.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
-- Copyright The OpenTelemetry Authors
-- SPDX-License-Identifier: Apache-2.0

CREATE TABLE public.featureflags (
CREATE TABLE IF NOT EXISTS public.featureflags (
name character varying(255),
description character varying(255),
enabled double precision DEFAULT 0.0 NOT NULL
);

ALTER TABLE ONLY public.featureflags DROP CONSTRAINT IF EXISTS featureflags_pkey;
ALTER TABLE ONLY public.featureflags ADD CONSTRAINT featureflags_pkey PRIMARY KEY (name);

CREATE UNIQUE INDEX featureflags_name_index ON public.featureflags USING btree (name);
CREATE UNIQUE INDEX IF NOT EXISTS featureflags_name_index ON public.featureflags USING btree (name);

9 changes: 8 additions & 1 deletion src/ffspostgres/init-scripts/20-ffs_data.sql
Original file line number Diff line number Diff line change
@@ -7,4 +7,11 @@ VALUES
('productCatalogFailure', 'Fail product catalog service on a specific product', 0),
('recommendationCache', 'Cache recommendations', 0),
('adServiceFailure', 'Fail ad service requests', 0),
('cartServiceFailure', 'Fail cart service requests', 0);
('cartServiceFailure', 'Fail cart service requests', 0),
('paymentServiceSimulateSlowness', 'Simulate slow response times in the payment service', 0),
('paymentServiceSimulateSlownessLowerBound', 'Minimum simulated delay in milliseconds in payment service, if enabled', 200),
('paymentServiceSimulateSlownessUpperBound', 'Maximum simulated delay in milliseconds in payment service, if enabled', 600),
('shippingServiceSimulateSlowness', 'Simulate slow response times in the shipping service', 0),
('shippingServiceSimulateSlownessLowerBound', 'Minimum simulated delay in milliseconds in shipping service, if enabled', 250),
('shippingServiceSimulateSlownessUpperBound', 'Maximum simulated delay in milliseconds in shipping service, if enabled', 400)
ON CONFLICT DO NOTHING;
1 change: 1 addition & 0 deletions src/frontend/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
75 changes: 75 additions & 0 deletions src/frontend/gateways/rpc/FeatureFlag.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

import {ChannelCredentials, status} from '@grpc/grpc-js';
import {
CreateFlagRequest,
CreateFlagResponse,
DeleteFlagResponse, EvaluateProbabilityFeatureFlagResponse,
FeatureFlagServiceClient, GetFeatureFlagValueResponse,
ListFlagsResponse, UpdateFlagValueRequest, UpdateFlagValueResponse,
} from '../../protos/demo';

const {FEATURE_FLAG_GRPC_SERVICE_ADDR = ''} = process.env;

const client = new FeatureFlagServiceClient(FEATURE_FLAG_GRPC_SERVICE_ADDR, ChannelCredentials.createInsecure());

const FeatureFlagGateway = () => ({

getFeatureFlagValue(name: string) {
return new Promise<GetFeatureFlagValueResponse>((resolve, reject) =>
client.getFeatureFlagValue({name}, (error, response) => (error ? reject(error) : resolve(response)))
);
},

evaluateProbabilityFeatureFlag(name: string) {
return new Promise<EvaluateProbabilityFeatureFlagResponse>((resolve, reject) =>
client.evaluateProbabilityFeatureFlag({name}, (error, response) => (error ? reject(error) : resolve(response)))
);
},

getRangeFeatureFlag(name: string) {
return new Promise<EvaluateProbabilityFeatureFlagResponse>((resolve, reject) =>
client.getRangeFeatureFlag({
name,
nameLowerBound: `${name}LowerBound`,
nameUpperBound: `${name}UpperBound`
}, (error, response) => (error ? reject(error) : resolve(response)))
);
},

createFlag(flag: CreateFlagRequest) {
return new Promise<CreateFlagResponse>((resolve, reject) =>
client.createFlag(flag, (error, response) => (error ? reject(error) : resolve(response)))
);
},

updateFlagValue(name: string, flag: UpdateFlagValueRequest) {
return new Promise<UpdateFlagValueResponse>((resolve, reject) => client.updateFlagValue(flag, (error, response) => (error ? reject(error) : resolve(response)))
);
},

listFlags() {
return new Promise<ListFlagsResponse>((resolve, reject) =>
client.listFlags({}, (error, response) => (error ? reject(error) : resolve(response)))
);
},

deleteFlag(name: string) {
return new Promise<DeleteFlagResponse>((resolve, reject) => client.deleteFlag({name},
(error, response) => {
if (error) {
if (error.code === status.NOT_FOUND) {
return resolve({} as DeleteFlagResponse)
} else {
return reject(error)
}
}
resolve(response)
}
)
);
},
});

export default FeatureFlagGateway();
4 changes: 2 additions & 2 deletions src/frontend/gateways/rpc/Shipping.gateway.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ const { SHIPPING_SERVICE_ADDR = '' } = process.env;

const client = new ShippingServiceClient(SHIPPING_SERVICE_ADDR, ChannelCredentials.createInsecure());

const AdGateway = () => ({
const ShippingGateway = () => ({
getShippingCost(itemList: CartItem[], address: Address) {
return new Promise<GetQuoteResponse>((resolve, reject) =>
client.getQuote({ items: itemList, address: address }, (error, response) =>
@@ -18,4 +18,4 @@ const AdGateway = () => ({
},
});

export default AdGateway();
export default ShippingGateway();
777 changes: 481 additions & 296 deletions src/frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@
"next": "12.3.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"sharp": "0.32.6",
"sharp": "0.33.2",
"styled-components": "6.1.1",
"ts-proto": "1.164.0",
"uuid": "9.0.1"
@@ -48,8 +48,8 @@
"@types/node": "20.9.0",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/uuid": "9.0.7",
"@types/styled-components": "5.1.30",
"@types/uuid": "9.0.7",
"@typescript-eslint/eslint-plugin": "6.10.0",
"@typescript-eslint/parser": "6.10.0",
"cypress": "10.11.0",
62 changes: 62 additions & 0 deletions src/frontend/pages/api/featureflags/[name]/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

import type {NextApiRequest, NextApiResponse} from 'next';
import FeatureFlagGateway from '../../../../gateways/rpc/FeatureFlag.gateway';
import {
Empty,
EvaluateProbabilityFeatureFlagResponse,
GetFeatureFlagValueResponse,
UpdateFlagValueRequest,
} from '../../../../protos/demo';

type TResponse = Empty | GetFeatureFlagValueResponse | EvaluateProbabilityFeatureFlagResponse;

const handler = async ({method, query, body}: NextApiRequest, res: NextApiResponse<TResponse>) => {
switch (method) {
case 'GET': {
const {name = '', mode = 'raw'} = query
switch (mode as string) {
case 'probability':
const randomDecisionOutcome = await FeatureFlagGateway.evaluateProbabilityFeatureFlag(name as string);
return res.status(200).json(randomDecisionOutcome);
case 'range':
const range = await FeatureFlagGateway.getRangeFeatureFlag(name as string);
return res.status(200).json(range);
case 'raw':
// fall through
default:
const flag = await FeatureFlagGateway.getFeatureFlagValue(name as string);
return res.status(200).json(flag);
}
}

case 'PUT': {
const {name} = query
if (!name || Array.isArray(name)) {
return res.status(400).end()
}
if (!body || typeof body.value !== 'number') {
return res.status(400).end()
}
const updateFlagProbabilityRequest = body as UpdateFlagValueRequest;
// The name is part of the resource path, and we do not want to require clients to repeat the name in the request
// body; but on the grpc level the name needs to be in the message body, so we move it there.
updateFlagProbabilityRequest.name = name;
await FeatureFlagGateway.updateFlagValue(name as string, updateFlagProbabilityRequest);
return res.status(204).end();
}

case 'DELETE': {
const {name = ''} = query
await FeatureFlagGateway.deleteFlag(name as string);
return res.status(204).end();
}

default: {
return res.status(405).end();
}
}
};

export default handler;
35 changes: 35 additions & 0 deletions src/frontend/pages/api/featureflags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

import type {NextApiRequest, NextApiResponse} from 'next';
import FeatureFlagGateway from '../../../gateways/rpc/FeatureFlag.gateway';
import {CreateFlagRequest, Empty, FlagDefinition} from '../../../protos/demo';

type TResponse = FlagDefinition[] | FlagDefinition | Empty;

const handler = async ({method, body}: NextApiRequest, res: NextApiResponse<TResponse>) => {
switch (method) {
case 'POST': {
if (!body || typeof body.name !== 'string' || typeof body.value !== 'number') {
return res.status(400).end()
}
if (!body.description) {
body.description = '-';
}
const flagFromRequest = body as CreateFlagRequest
await FeatureFlagGateway.createFlag(flagFromRequest);
return res.status(204).end();
}

case 'GET': {
const {flag: allFlags} = await FeatureFlagGateway.listFlags();
return res.status(200).json(allFlags);
}

default: {
return res.status(405).send('');
}
}
};

export default handler;
2 changes: 2 additions & 0 deletions src/paymentservice/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.yaml
*.md
6 changes: 6 additions & 0 deletions src/paymentservice/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
}
93 changes: 75 additions & 18 deletions src/paymentservice/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict'

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
const { callbackify, promisify } = require('util')
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const health = require('grpc-js-health-check')
@@ -8,52 +11,106 @@ const opentelemetry = require('@opentelemetry/api')
const charge = require('./charge')
const logger = require('./logger')

function chargeServiceHandler(call, callback) {
const span = opentelemetry.trace.getActiveSpan();
const otelDemoPackage = grpc.loadPackageDefinition(
protoLoader.loadSync('demo.proto')
)

const featureFlagServiceClient = initFeatureFlagClient()
const getPaymentServiceSimulateSlowness = featureFlagServiceClient
? promisify(featureFlagServiceClient.getRangeFeatureFlag.bind(featureFlagServiceClient))
: null

async function chargeServiceHandler(call) {
const span = opentelemetry.trace.getActiveSpan()

try {
const amount = call.request.amount
span.setAttributes({
'app.payment.amount': parseFloat(`${amount.units}.${amount.nanos}`)
})
logger.info({ request: call.request }, "Charge request received.")

const response = charge.charge(call.request)
callback(null, response)
logger.info({ request: call.request }, 'Charge request received.')
await simulateSlowness(span);

return charge.charge(call.request)
} catch (err) {
logger.warn({ err })

span.recordException(err)
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR })

callback(err)
throw err
}
}

async function simulateSlowness(span) {
if (getPaymentServiceSimulateSlowness) {
try {
const simulateSlownessResponse = await getPaymentServiceSimulateSlowness({
name: 'paymentServiceSimulateSlowness',
nameLowerBound: 'paymentServiceSimulateSlownessLowerBound',
nameUpperBound: 'paymentServiceSimulateSlownessUpperBound'
})
if (simulateSlownessResponse.enabled) {
const minimumDelayResponse = simulateSlownessResponse.lowerBound || 0
const maximumDelayResponse = simulateSlownessResponse.upperBound || 500
const delayMillis =
minimumDelayResponse +
Math.floor(Math.random() * (maximumDelayResponse - minimumDelayResponse))
logger.info('Simulating payment service slowness, waiting %d.', delayMillis)
span.setAttributes({ 'app.payment.simulatedSlowness': delayMillis })
await new Promise((resolve) => setTimeout(resolve, delayMillis))
}
} catch (err) {
logger.warn({err: err})
}
}
}

async function closeGracefully(signal) {
featureFlagServiceClient.close()
server.forceShutdown()
process.kill(process.pid, signal)
}

const otelDemoPackage = grpc.loadPackageDefinition(protoLoader.loadSync('demo.proto'))
const server = new grpc.Server()

server.addService(health.service, new health.Implementation({
'': health.servingStatus.SERVING
}))
server.addService(
health.service,
new health.Implementation({
'': health.servingStatus.SERVING,
})
)

server.addService(otelDemoPackage.oteldemo.PaymentService.service, {
charge: callbackify(chargeServiceHandler),
})

server.addService(otelDemoPackage.oteldemo.PaymentService.service, { charge: chargeServiceHandler })
server.bindAsync(
`0.0.0.0:${process.env['PAYMENT_SERVICE_PORT']}`,
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
return logger.error({ err })
}

server.bindAsync(`0.0.0.0:${process.env['PAYMENT_SERVICE_PORT']}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
if (err) {
return logger.error({ err })
logger.info(`PaymentService gRPC server started on port ${port}`)
server.start()
}
)

logger.info(`PaymentService gRPC server started on port ${port}`)
server.start()
function initFeatureFlagClient() {
const featureFlagServiceAddress = process.env.FEATURE_FLAG_GRPC_SERVICE_ADDR
if (!featureFlagServiceAddress) {
logger.warn(
'The feature flag service address is not set (FEATURE_FLAG_GRPC_SERVICE_ADDR). No artificial request duration variance will be introduced.'
)
return null
}
return new otelDemoPackage.oteldemo.FeatureFlagService(
featureFlagServiceAddress,
grpc.credentials.createInsecure()
)
}
)

process.once('SIGINT', closeGracefully)
process.once('SIGTERM', closeGracefully)
2 changes: 1 addition & 1 deletion src/productcatalogservice/Dockerfile
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app/
RUN apk add build-base protobuf-dev protoc

COPY ./src/productcatalogservice/ ./
RUN go build -o /go/bin/productcatalogservice/
RUN GOTOOLCHAIN=local go build -o /go/bin/productcatalogservice/

# -----------------------------------------------------------------------------

959 changes: 632 additions & 327 deletions src/productcatalogservice/genproto/oteldemo/demo.pb.go

Large diffs are not rendered by default.

176 changes: 142 additions & 34 deletions src/productcatalogservice/genproto/oteldemo/demo_grpc.pb.go

Large diffs are not rendered by default.

51 changes: 27 additions & 24 deletions src/productcatalogservice/go.mod
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
module github.com/opentelemetry/opentelemetry-demo/src/productcatalogservice

go 1.22
go 1.22.0

require (
github.com/sirupsen/logrus v1.9.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
go.opentelemetry.io/otel v1.23.1
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1
go.opentelemetry.io/otel/sdk v1.23.1
go.opentelemetry.io/otel/sdk/metric v1.23.1
go.opentelemetry.io/otel/trace v1.23.1
google.golang.org/grpc v1.61.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.32.0
go.opentelemetry.io/contrib/bridges/otellogrus v0.10.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/sdk/metric v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
google.golang.org/grpc v1.71.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
google.golang.org/protobuf v1.36.5
)

require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 // indirect
go.opentelemetry.io/otel/metric v1.23.1 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/log v0.11.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
)
127 changes: 58 additions & 69 deletions src/productcatalogservice/go.sum
Original file line number Diff line number Diff line change
@@ -1,87 +1,76 @@
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 h1:P+/g8GpuJGYbOp2tAdKrIPUX9JO02q8Q0YNlHolpibA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg=
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU=
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs=
go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqRWZJGHXV/1yCcEEVJ6/Uz2JtM79DNS8OZYa3vVY/A=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1 h1:p3A5+f5l9e/kuEBwLOrnpkIDHQFlHmbiVxMURWRK6gQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1/go.mod h1:OClrnXUjBqQbInvjJFjYSnMxBSCXBF8r3b34WqjiIrQ=
go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E=
go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk=
go.opentelemetry.io/otel/sdk/metric v1.23.1 h1:T9/8WsYg+ZqIpMWwdISVVrlGb/N0Jr1OHjR/alpKwzg=
go.opentelemetry.io/otel/sdk/metric v1.23.1/go.mod h1:8WX6WnNtHCgUruJ4TJ+UssQjMtpxkpX0zveQC8JG/E0=
go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otellogrus v0.10.0 h1:MbVh3+6Y1zKAZmRfj3qxiV9pX3xF4s45fMYEKq5AB5U=
go.opentelemetry.io/contrib/bridges/otellogrus v0.10.0/go.mod h1:DvLmmLHXKIoU9uEeCZI3euWbiD7GSObF/cCiOu8hvW0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
31 changes: 24 additions & 7 deletions src/productcatalogservice/main.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import (
"time"

"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/bridges/otellogrus"

"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/contrib/instrumentation/runtime"
@@ -53,6 +54,19 @@ var (

func init() {
log = logrus.New()

// Add OpenTelemetry hook to send logs to the collector
log.AddHook(otellogrus.NewHook(
"productcatalogservice",
otellogrus.WithLevels([]logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
logrus.InfoLevel,
}),
))

var err error
catalog, err = readProductFiles()
if err != nil {
@@ -83,7 +97,7 @@ func initTracerProvider() *sdktrace.TracerProvider {

exporter, err := otlptracegrpc.New(ctx)
if err != nil {
log.Fatalf("OTLP Trace gRPC Creation: %v", err)
log.WithContext(ctx).Fatalf("OTLP Trace gRPC Creation: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
@@ -99,7 +113,7 @@ func initMeterProvider() *sdkmetric.MeterProvider {

exporter, err := otlpmetricgrpc.New(ctx)
if err != nil {
log.Fatalf("new otlp metric grpc exporter failed: %v", err)
log.WithContext(ctx).Fatalf("new otlp metric grpc exporter failed: %v", err)
}

mp := sdkmetric.NewMeterProvider(
@@ -158,14 +172,14 @@ func main() {

go func() {
if err := srv.Serve(ln); err != nil {
log.Fatalf("Failed to serve gRPC server, err: %v", err)
log.WithContext(ctx).Fatalf("Failed to serve gRPC server, err: %v", err)
}
}()

<-ctx.Done()

srv.GracefulStop()
log.Println("ProductCatalogService gRPC server stopped")
log.WithContext(ctx).Println("ProductCatalogService gRPC server stopped")
}

type productCatalog struct {
@@ -250,6 +264,7 @@ func (p *productCatalog) GetProduct(ctx context.Context, req *pb.GetProductReque
msg := fmt.Sprintf("Error: ProductCatalogService Fail Feature Flag Enabled")
span.SetStatus(otelcodes.Error, msg)
span.AddEvent(msg)
log.WithContext(ctx).WithField("request.id", req.Id).Println("ProductCatalogService Fail Feature Flag Enabled")
return nil, status.Errorf(codes.Internal, msg)
}

@@ -265,6 +280,7 @@ func (p *productCatalog) GetProduct(ctx context.Context, req *pb.GetProductReque
msg := fmt.Sprintf("Product Not Found: %s", req.Id)
span.SetStatus(otelcodes.Error, msg)
span.AddEvent(msg)
log.WithContext(ctx).WithField("request.id", req.Id).Println("Product Not Found")
return nil, status.Errorf(codes.NotFound, msg)
}

@@ -273,6 +289,7 @@ func (p *productCatalog) GetProduct(ctx context.Context, req *pb.GetProductReque
span.SetAttributes(
attribute.String("app.product.name", found.Name),
)
log.WithContext(ctx).WithField("app.product.name", found.Name).Println(msg)
return found, nil
}

@@ -306,16 +323,16 @@ func (p *productCatalog) checkProductFailure(ctx context.Context, id string) boo
defer conn.Close()

flagName := "productCatalogFailure"
ffResponse, err := pb.NewFeatureFlagServiceClient(conn).GetFlag(ctx, &pb.GetFlagRequest{
ffResponse, err := pb.NewFeatureFlagServiceClient(conn).EvaluateProbabilityFeatureFlag(ctx, &pb.EvaluateProbabilityFeatureFlagRequest{
Name: flagName,
})
if err != nil {
span := trace.SpanFromContext(ctx)
span.AddEvent("error", trace.WithAttributes(attribute.String("message", fmt.Sprintf("GetFlag Failed: %s", flagName))))
span.AddEvent("error", trace.WithAttributes(attribute.String("message", fmt.Sprintf("EvaluateProbabilityFeatureFlag Failed: %s", flagName))))
return false
}

return ffResponse.GetFlag().Enabled
return ffResponse.Enabled
}

func createClient(ctx context.Context, svcAddr string) (*grpc.ClientConn, error) {
Binary file added src/productcatalogservice/productcatalogservice
Binary file not shown.
2 changes: 1 addition & 1 deletion src/recommendationservice/logger.py
Original file line number Diff line number Diff line change
@@ -23,6 +23,6 @@ def getJSONLogger(name):
formatter = CustomJsonFormatter('%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s] - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.setLevel(logging.DEBUG)
logger.propagate = False
return logger
8 changes: 6 additions & 2 deletions src/recommendationservice/recommendation_server.py
Original file line number Diff line number Diff line change
@@ -61,6 +61,7 @@ def Watch(self, request, context):


def get_product_list(request_product_ids):
logger.debug("get_product_list: start")
global first_run
global cached_ids
with tracer.start_as_current_span("get_product_list") as span:
@@ -71,7 +72,9 @@ def get_product_list(request_product_ids):
request_product_ids = request_product_ids_str.split(',')

# Feature flag scenario - Cache Leak
logger.debug("get_product_list: checking recommendationCache feature flag")
if check_feature_flag("recommendationCache"):
logger.debug("get_product_list: recommendationCache feature flag is enabled")
span.set_attribute("app.recommendation.cache_enabled", True)
if random.random() < 0.5 or first_run:
first_run = False
@@ -87,6 +90,7 @@ def get_product_list(request_product_ids):
logger.info("get_product_list: cache hit")
product_ids = cached_ids
else:
logger.debug("get_product_list: recommendationCache feature flag is not enabled")
span.set_attribute("app.recommendation.cache_enabled", False)
cat_response = product_catalog_stub.ListProducts(demo_pb2.Empty())
product_ids = [x.id for x in cat_response.products]
@@ -106,6 +110,7 @@ def get_product_list(request_product_ids):

span.set_attribute("app.filtered_products.list", prod_list)

logger.debug("get_product_list: done")
return prod_list


@@ -119,8 +124,7 @@ def must_map_env(key: str):
def check_feature_flag(flag_name: str):
if feature_flag_stub is None:
return False
flag = feature_flag_stub.GetFlag(demo_pb2.GetFlagRequest(name=flag_name)).flag
return flag.enabled
return feature_flag_stub.EvaluateProbabilityFeatureFlag(demo_pb2.EvaluateProbabilityFeatureFlagRequest(name=flag_name)).enabled


if __name__ == "__main__":
2 changes: 2 additions & 0 deletions src/shippingservice/Cargo.lock
2 changes: 2 additions & 0 deletions src/shippingservice/Cargo.toml
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@ reqwest-tracing = { version = "0.4.6", features = ["opentelemetry_0_20"] }
tracing = { version = "0.1", features = ["max_level_debug", "release_max_level_info"] }
tracing-opentelemetry = "0.22.0"
tracing-subscriber = "0.3"
time = "0.3.17"
rand = "0.8.5"

[dependencies.uuid]
version = "1.5.0"
37 changes: 34 additions & 3 deletions src/shippingservice/src/main.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ use opentelemetry_sdk::{propagation::TraceContextPropagator, resource::{
}, runtime, trace as sdktrace};
use opentelemetry_otlp::{self, WithExportConfig};

use tonic::transport::Server;
use tonic::transport::{Channel, Server};

use tracing_subscriber::Registry;
use tracing_subscriber::layer::SubscriberExt;
@@ -22,8 +22,15 @@ use std::env;
use std::time::Duration;

mod shipping_service;

use shipping_service::shop::shipping_service_server::ShippingServiceServer;
use shipping_service::ShippingServer;
use shop::feature_flag_service_client::FeatureFlagServiceClient;

pub mod shop {
// The string specified here must match the proto package name
tonic::include_proto!("oteldemo");
}

fn init_logger() -> Result<(), log::SetLoggerError> {
CombinedLogger::init(vec![
@@ -67,6 +74,28 @@ fn init_reqwest_tracing(tracer: sdktrace::Tracer) -> Result<(), tracing::subscri
tracing::subscriber::set_global_default(subscriber)
}

async fn init_feature_flag_client() -> Option<FeatureFlagServiceClient<Channel>> {
let ffs_addr_env = env::var("FEATURE_FLAG_GRPC_SERVICE_ADDR");
if ffs_addr_env.is_ok() {
let addr = ffs_addr_env.unwrap();
let addr_with_scheme = "http://".to_owned() + addr.as_str();
info!("Trying to connect to feature flag service at: {}", addr_with_scheme);
let result = Channel::from_shared(addr_with_scheme.clone());
if result.is_ok() {
let ffs_channel = result.ok()?.connect().await;
if ffs_channel.is_ok() {
let ffc = FeatureFlagServiceClient::new(ffs_channel.ok()?);
info!("Connected to feature flag service at: {}", addr_with_scheme);
return Some(ffc);
}
warn!("Could not connect to feature flag service at: {}, simulated slowness will not be enabled.", addr_with_scheme);
}
} else {
warn!("FEATURE_FLAG_GRPC_SERVICE_ADDR is not set, simulated slowness will not be enabled.");
}
return None
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
@@ -76,11 +105,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

init_logger()?;
init_reqwest_tracing(init_tracer()?)?;
let feature_flag_client = init_feature_flag_client().await;

info!("OTel pipeline created");
let port = env::var("SHIPPING_SERVICE_PORT").expect("$SHIPPING_SERVICE_PORT is not set");
let addr = format!("0.0.0.0:{}", port).parse()?;
info!("listening on {}", addr);
let shipper = ShippingServer::default();
let shipper = ShippingServer::new(feature_flag_client);

Server::builder()
.add_service(ShippingServiceServer::new(shipper))
@@ -89,4 +120,4 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;

Ok(())
}
}
57 changes: 54 additions & 3 deletions src/shippingservice/src/shipping_service.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

use std::{thread, time};
use rand::Rng;

use opentelemetry::{global, propagation::Extractor, trace::Span, Context, KeyValue};
use opentelemetry::trace::{FutureExt, TraceContextExt, SpanKind, Tracer};
use opentelemetry_semantic_conventions as semcov;
use shop::shipping_service_server::ShippingService;
use shop::{GetQuoteRequest, GetQuoteResponse, Money, ShipOrderRequest, ShipOrderResponse};
use tonic::{Request, Response, Status};

use log::*;
use tonic::transport::Channel;

use shop::{GetQuoteRequest, GetQuoteResponse, Money, ShipOrderRequest, ShipOrderResponse};
use crate::shop::feature_flag_service_client::FeatureFlagServiceClient;
use crate::shop::{RangeFeatureFlagRequest, RangeFeatureFlagResponse};

mod quote;
use quote::create_quote_from_count;
@@ -23,11 +30,14 @@ const RPC_GRPC_STATUS_CODE_OK: i64 = 0;
const RPC_GRPC_STATUS_CODE_UNKNOWN: i64 = 2;

pub mod shop {
tonic::include_proto!("oteldemo"); // The string specified here must match the proto package name
// The string specified here must match the proto package name
tonic::include_proto!("oteldemo");
}

#[derive(Debug, Default)]
pub struct ShippingServer {}
pub struct ShippingServer {
feature_flag_client: Option<FeatureFlagServiceClient<Channel>>,
}

struct MetadataMap<'a>(&'a tonic::metadata::MetadataMap);

@@ -85,6 +95,8 @@ impl ShippingService for ShippingServer {
Err(status) => {cx.span().set_attribute(semcov::trace::RPC_GRPC_STATUS_CODE.i64(RPC_GRPC_STATUS_CODE_UNKNOWN)); return Err(status)},
};

self.simulate_slowness().await;

let reply = GetQuoteResponse {
cost_usd: Some(Money {
currency_code: "USD".into(),
@@ -114,6 +126,8 @@ impl ShippingService for ShippingServer {

span.add_event("Processing shipping order request".to_string(), vec![]);

self.simulate_slowness().await;

let tid = create_tracking_id();
span.set_attribute(KeyValue::new("app.shipping.tracking.id", tid.clone()));
info!("Tracking ID Created: {}", tid);
@@ -128,6 +142,42 @@ impl ShippingService for ShippingServer {
}
}

impl ShippingServer {

pub fn new(feature_flag_client: Option<FeatureFlagServiceClient<Channel>>) -> Self {
Self { feature_flag_client }
}

async fn simulate_slowness(&self) {
if self.feature_flag_client.is_none() {
return;
}

let mut client = self.feature_flag_client.clone().unwrap();

let request = Request::new(
RangeFeatureFlagRequest {
name: String::from("shippingServiceSimulateSlowness"),
name_lower_bound: String::from("shippingServiceSimulateSlownessLowerBound"),
name_upper_bound: String::from("shippingServiceSimulateSlownessUpperBound"),
},
);


let response: Response<RangeFeatureFlagResponse> =
client.get_range_feature_flag(request).await.unwrap();
let range_response: RangeFeatureFlagResponse = response.into_inner();
if range_response.enabled {
let minimum_delay = range_response.lower_bound.floor();
let maximum_delay = range_response.upper_bound.floor();
let delay_millis = rand::thread_rng().gen_range(minimum_delay..maximum_delay).floor();
info!("Simulating shipping service slowness, waiting {}.", delay_millis);
let sleep_time = time::Duration::from_millis(delay_millis as u64);
thread::sleep(sleep_time);
}
}
}

#[cfg(test)]
mod tests {
use super::{
@@ -157,6 +207,7 @@ mod tests {
fn make_empty_quote_request() -> Request<GetQuoteRequest> {
Request::new(GetQuoteRequest::default())
}

#[tokio::test]
async fn empty_quote() {
let server = ShippingServer::default();