mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -33,29 +33,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./Home/Dockerfile .
|
||||
|
||||
docker-build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./Worker/Dockerfile .
|
||||
|
||||
|
||||
docker-build-app:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -129,29 +106,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./Probe/Dockerfile .
|
||||
|
||||
docker-build-telemetry:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Preinstall
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build --no-cache -f ./Telemetry/Dockerfile .
|
||||
|
||||
docker-build-test-server:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
35
.github/workflows/compile.yml
vendored
35
.github/workflows/compile.yml
vendored
@@ -77,23 +77,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: cd Home && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-worker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- name: Compile Worker
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Worker && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-nginx:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -201,24 +184,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: cd Probe && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-telemetry:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- name: Compile Telemetry
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Telemetry && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-status-page:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
183
.github/workflows/release.yml
vendored
183
.github/workflows/release.yml
vendored
@@ -569,88 +569,6 @@ jobs:
|
||||
--image test \
|
||||
--tags "${SANITIZED_VERSION},enterprise-${SANITIZED_VERSION}"
|
||||
|
||||
telemetry-docker-image-build:
|
||||
needs: [generate-build-number, read-version]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image telemetry \
|
||||
--version "${{needs.read-version.outputs.major_minor}}" \
|
||||
--dockerfile ./Telemetry/Dockerfile \
|
||||
--context . \
|
||||
--platforms ${{ matrix.platform }} \
|
||||
--git-sha "${{ github.sha }}"
|
||||
|
||||
telemetry-docker-image-merge:
|
||||
needs: [telemetry-docker-image-build, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Merge multi-arch manifests
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}"
|
||||
SANITIZED_VERSION="${VERSION//+/-}"
|
||||
bash ./Scripts/GHA/merge_docker_manifests.sh \
|
||||
--image telemetry \
|
||||
--tags "${SANITIZED_VERSION},enterprise-${SANITIZED_VERSION}"
|
||||
|
||||
probe-docker-image-build:
|
||||
needs: [generate-build-number, read-version]
|
||||
strategy:
|
||||
@@ -921,88 +839,6 @@ jobs:
|
||||
- name: Publish NPM Packages
|
||||
run: bash ./Scripts/NPM/PublishAllPackages.sh
|
||||
|
||||
worker-docker-image-build:
|
||||
needs: [generate-build-number, read-version]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image worker \
|
||||
--version "${{needs.read-version.outputs.major_minor}}" \
|
||||
--dockerfile ./Worker/Dockerfile \
|
||||
--context . \
|
||||
--platforms ${{ matrix.platform }} \
|
||||
--git-sha "${{ github.sha }}"
|
||||
|
||||
worker-docker-image-merge:
|
||||
needs: [worker-docker-image-build, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Merge multi-arch manifests
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}"
|
||||
SANITIZED_VERSION="${VERSION//+/-}"
|
||||
bash ./Scripts/GHA/merge_docker_manifests.sh \
|
||||
--image worker \
|
||||
--tags "${SANITIZED_VERSION},enterprise-${SANITIZED_VERSION}"
|
||||
|
||||
# ─── Non-Docker jobs (downstream dependencies updated) ───────────────
|
||||
|
||||
publish-terraform-provider:
|
||||
@@ -1019,8 +855,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history for changelog generation
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -1061,7 +895,7 @@ jobs:
|
||||
gpg --export-secret-keys >~/.gnupg/secring.gpg
|
||||
echo "GPG key exported successfully"
|
||||
|
||||
- name: Generate Terraform provider
|
||||
- name: Generate and publish Terraform provider
|
||||
run: npm run publish-terraform-provider -- --version "${{ steps.version.outputs.version }}" --github-token "${{ secrets.SIMLARSEN_GITHUB_PAT }}" --github-repo-deploy-key "${{ secrets.TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY }}"
|
||||
|
||||
|
||||
@@ -1076,11 +910,9 @@ jobs:
|
||||
- home-docker-image-merge
|
||||
- test-server-docker-image-merge
|
||||
- test-docker-image-merge
|
||||
- telemetry-docker-image-merge
|
||||
- probe-docker-image-merge
|
||||
- app-docker-image-merge
|
||||
- ai-agent-docker-image-merge
|
||||
- worker-docker-image-merge
|
||||
- test-e2e-release-saas
|
||||
- test-e2e-release-self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1093,11 +925,9 @@ jobs:
|
||||
"home",
|
||||
"test-server",
|
||||
"test",
|
||||
"telemetry",
|
||||
"probe",
|
||||
"app",
|
||||
"ai-agent",
|
||||
"worker"
|
||||
"ai-agent"
|
||||
]
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
@@ -1143,7 +973,7 @@ jobs:
|
||||
|
||||
test-e2e-release-saas:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [telemetry-docker-image-merge, ai-agent-docker-image-merge, app-docker-image-merge, home-docker-image-merge, worker-docker-image-merge, probe-docker-image-merge, test-docker-image-merge, test-server-docker-image-merge, publish-npm-packages, e2e-docker-image-merge, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-merge]
|
||||
needs: [ai-agent-docker-image-merge, app-docker-image-merge, home-docker-image-merge, probe-docker-image-merge, test-docker-image-merge, test-server-docker-image-merge, publish-npm-packages, e2e-docker-image-merge, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-merge]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
@@ -1274,7 +1104,7 @@ jobs:
|
||||
test-e2e-release-self-hosted:
|
||||
runs-on: ubuntu-latest
|
||||
# After all the jobs runs
|
||||
needs: [telemetry-docker-image-merge, ai-agent-docker-image-merge, app-docker-image-merge, home-docker-image-merge, worker-docker-image-merge, probe-docker-image-merge, test-docker-image-merge, test-server-docker-image-merge, publish-npm-packages, e2e-docker-image-merge, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-merge]
|
||||
needs: [ai-agent-docker-image-merge, app-docker-image-merge, home-docker-image-merge, probe-docker-image-merge, test-docker-image-merge, test-server-docker-image-merge, publish-npm-packages, e2e-docker-image-merge, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-merge]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
@@ -1875,8 +1705,9 @@ jobs:
|
||||
MARKETING_VERSION=${{ needs.read-version.outputs.major_minor }} \
|
||||
CURRENT_PROJECT_VERSION=${{ needs.generate-build-number.outputs.build_number }} \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
OTHER_CODE_SIGN_FLAGS="--keychain ${{ env.KEYCHAIN_PATH }}" \
|
||||
-allowProvisioningUpdates
|
||||
CODE_SIGN_IDENTITY="Apple Distribution" \
|
||||
PROVISIONING_PROFILE_SPECIFIER="${{ env.PROFILE_UUID }}" \
|
||||
OTHER_CODE_SIGN_FLAGS="--keychain ${{ env.KEYCHAIN_PATH }}"
|
||||
|
||||
- name: Export IPA
|
||||
run: |
|
||||
|
||||
170
.github/workflows/test-release.yaml
vendored
170
.github/workflows/test-release.yaml
vendored
@@ -514,90 +514,6 @@ jobs:
|
||||
--image test \
|
||||
--tags "${SANITIZED_VERSION},test,enterprise-${SANITIZED_VERSION},enterprise-test"
|
||||
|
||||
telemetry-docker-image-build:
|
||||
needs: [read-version, generate-build-number]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image telemetry \
|
||||
--version "${{needs.read-version.outputs.major_minor}}-test" \
|
||||
--dockerfile ./Telemetry/Dockerfile \
|
||||
--context . \
|
||||
--platforms ${{ matrix.platform }} \
|
||||
--git-sha "${{ github.sha }}" \
|
||||
--extra-tags test \
|
||||
--extra-enterprise-tags enterprise-test
|
||||
|
||||
telemetry-docker-image-merge:
|
||||
needs: [telemetry-docker-image-build, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Merge multi-arch manifests
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}-test"
|
||||
SANITIZED_VERSION="${VERSION//+/-}"
|
||||
bash ./Scripts/GHA/merge_docker_manifests.sh \
|
||||
--image telemetry \
|
||||
--tags "${SANITIZED_VERSION},test,enterprise-${SANITIZED_VERSION},enterprise-test"
|
||||
|
||||
probe-docker-image-build:
|
||||
needs: [read-version, generate-build-number]
|
||||
strategy:
|
||||
@@ -850,90 +766,6 @@ jobs:
|
||||
--image ai-agent \
|
||||
--tags "${SANITIZED_VERSION},test,enterprise-${SANITIZED_VERSION},enterprise-test"
|
||||
|
||||
worker-docker-image-build:
|
||||
needs: [read-version, generate-build-number]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image worker \
|
||||
--version "${{needs.read-version.outputs.major_minor}}-test" \
|
||||
--dockerfile ./Worker/Dockerfile \
|
||||
--context . \
|
||||
--platforms ${{ matrix.platform }} \
|
||||
--git-sha "${{ github.sha }}" \
|
||||
--extra-tags test \
|
||||
--extra-enterprise-tags enterprise-test
|
||||
|
||||
worker-docker-image-merge:
|
||||
needs: [worker-docker-image-build, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Merge multi-arch manifests
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}-test"
|
||||
SANITIZED_VERSION="${VERSION//+/-}"
|
||||
bash ./Scripts/GHA/merge_docker_manifests.sh \
|
||||
--image worker \
|
||||
--tags "${SANITIZED_VERSION},test,enterprise-${SANITIZED_VERSION},enterprise-test"
|
||||
|
||||
# ─── Non-Docker jobs (unchanged) ─────────────────────────────────────
|
||||
|
||||
publish-terraform-provider:
|
||||
@@ -951,7 +783,7 @@ jobs:
|
||||
|
||||
test-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infrastructure-agent-deploy, publish-terraform-provider, telemetry-docker-image-merge, worker-docker-image-merge, home-docker-image-merge, test-server-docker-image-merge, test-docker-image-merge, probe-docker-image-merge, app-docker-image-merge, ai-agent-docker-image-merge, nginx-docker-image-merge, e2e-docker-image-merge]
|
||||
needs: [infrastructure-agent-deploy, publish-terraform-provider, home-docker-image-merge, test-server-docker-image-merge, test-docker-image-merge, probe-docker-image-merge, app-docker-image-merge, ai-agent-docker-image-merge, nginx-docker-image-merge, e2e-docker-image-merge]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
|
||||
22
.github/workflows/test.telemetry.yaml
vendored
22
.github/workflows/test.telemetry.yaml
vendored
@@ -1,22 +0,0 @@
|
||||
name: Telemetry Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'hotfix-*' # excludes hotfix branches
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Telemetry && npm install && npm run test
|
||||
|
||||
11
.github/workflows/test.yaml
vendored
11
.github/workflows/test.yaml
vendored
@@ -29,14 +29,3 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Home && npm install && npm run test
|
||||
|
||||
test-worker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Worker && npm install && npm run test
|
||||
35
App/API/Metrics.ts
Normal file
35
App/API/Metrics.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import AppQueueService from "../Services/Queue/AppQueueService";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
/**
|
||||
* JSON metrics endpoint for KEDA autoscaling
|
||||
* Returns combined queue size (worker + workflow + telemetry) as JSON for KEDA metrics-api scaler
|
||||
*/
|
||||
router.get(
|
||||
"/metrics/queue-size",
|
||||
async (
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const queueSize: number = await AppQueueService.getQueueSize();
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.status(200).json({
|
||||
queueSize: queueSize,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,504 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import TelemetryException from "Common/Models/DatabaseModels/TelemetryException";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import TelemetryServiceElement from "../TelemetryService/TelemetryServiceElement";
|
||||
import TelemetryExceptionElement from "./ExceptionElement";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import AppLink from "../AppLink/AppLink";
|
||||
|
||||
interface ServiceExceptionSummary {
|
||||
service: Service;
|
||||
unresolvedCount: number;
|
||||
totalOccurrences: number;
|
||||
}
|
||||
|
||||
const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
|
||||
const [unresolvedCount, setUnresolvedCount] = useState<number>(0);
|
||||
const [resolvedCount, setResolvedCount] = useState<number>(0);
|
||||
const [archivedCount, setArchivedCount] = useState<number>(0);
|
||||
const [topExceptions, setTopExceptions] = useState<Array<TelemetryException>>(
|
||||
[],
|
||||
);
|
||||
const [serviceSummaries, setServiceSummaries] = useState<
|
||||
Array<ServiceExceptionSummary>
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
const projectId: ObjectID = ProjectUtil.getCurrentProjectId()!;
|
||||
|
||||
// Load counts, top exceptions, and services in parallel
|
||||
const [
|
||||
unresolvedResult,
|
||||
resolvedResult,
|
||||
archivedResult,
|
||||
topExceptionsResult,
|
||||
servicesResult,
|
||||
] = await Promise.all([
|
||||
ModelAPI.count({
|
||||
modelType: TelemetryException,
|
||||
query: {
|
||||
projectId,
|
||||
isResolved: false,
|
||||
isArchived: false,
|
||||
},
|
||||
}),
|
||||
ModelAPI.count({
|
||||
modelType: TelemetryException,
|
||||
query: {
|
||||
projectId,
|
||||
isResolved: true,
|
||||
isArchived: false,
|
||||
},
|
||||
}),
|
||||
ModelAPI.count({
|
||||
modelType: TelemetryException,
|
||||
query: {
|
||||
projectId,
|
||||
isArchived: true,
|
||||
},
|
||||
}),
|
||||
ModelAPI.getList({
|
||||
modelType: TelemetryException,
|
||||
query: {
|
||||
projectId,
|
||||
isResolved: false,
|
||||
isArchived: false,
|
||||
},
|
||||
select: {
|
||||
message: true,
|
||||
exceptionType: true,
|
||||
fingerprint: true,
|
||||
isResolved: true,
|
||||
isArchived: true,
|
||||
occuranceCount: true,
|
||||
lastSeenAt: true,
|
||||
firstSeenAt: true,
|
||||
environment: true,
|
||||
service: {
|
||||
name: true,
|
||||
serviceColor: true,
|
||||
} as any,
|
||||
},
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
sort: {
|
||||
occuranceCount: SortOrder.Descending,
|
||||
},
|
||||
}),
|
||||
ModelAPI.getList({
|
||||
modelType: Service,
|
||||
query: {
|
||||
projectId,
|
||||
},
|
||||
select: {
|
||||
serviceColor: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
name: SortOrder.Ascending,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
setUnresolvedCount(unresolvedResult);
|
||||
setResolvedCount(resolvedResult);
|
||||
setArchivedCount(archivedResult);
|
||||
setTopExceptions(topExceptionsResult.data || []);
|
||||
|
||||
const loadedServices: Array<Service> = servicesResult.data || [];
|
||||
|
||||
// Load unresolved exception counts per service
|
||||
const serviceExceptionCounts: Array<ServiceExceptionSummary> = [];
|
||||
|
||||
for (const service of loadedServices) {
|
||||
// Get unresolved exceptions for this service
|
||||
const serviceExceptions: ListResult<TelemetryException> =
|
||||
await ModelAPI.getList({
|
||||
modelType: TelemetryException,
|
||||
query: {
|
||||
projectId,
|
||||
serviceId: service.id!,
|
||||
isResolved: false,
|
||||
isArchived: false,
|
||||
},
|
||||
select: {
|
||||
occuranceCount: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
occuranceCount: SortOrder.Descending,
|
||||
},
|
||||
});
|
||||
|
||||
const exceptions: Array<TelemetryException> =
|
||||
serviceExceptions.data || [];
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
let totalOccurrences: number = 0;
|
||||
|
||||
for (const ex of exceptions) {
|
||||
totalOccurrences += ex.occuranceCount || 0;
|
||||
}
|
||||
|
||||
serviceExceptionCounts.push({
|
||||
service,
|
||||
unresolvedCount: exceptions.length,
|
||||
totalOccurrences,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by unresolved count descending
|
||||
serviceExceptionCounts.sort(
|
||||
(a: ServiceExceptionSummary, b: ServiceExceptionSummary) => {
|
||||
return b.unresolvedCount - a.unresolvedCount;
|
||||
},
|
||||
);
|
||||
|
||||
setServiceSummaries(serviceExceptionCounts);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDashboard();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
onRefreshClick={() => {
|
||||
void loadDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const totalCount: number = unresolvedCount + resolvedCount + archivedCount;
|
||||
|
||||
if (totalCount === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
|
||||
<div className="text-gray-400 text-5xl mb-4">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0112 12.75zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 01-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 002.248-2.354M12 12.75a2.25 2.25 0 01-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 00-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 01.4-2.253M12 8.25a2.25 2.25 0 00-2.248 2.146M12 8.25a2.25 2.25 0 012.248 2.146M8.683 5a6.032 6.032 0 01-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0115.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 00-.575-1.752M4.921 12s-.148-.277-.277-.5M19.08 12s.147-.277.277-.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No exceptions caught yet
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 max-w-md mx-auto">
|
||||
Once your services start reporting exceptions, you{"'"}ll see a
|
||||
summary of bugs, their frequency, and which services are most
|
||||
affected.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<AppLink
|
||||
className="block"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
|
||||
)}
|
||||
>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Unresolved Bugs</p>
|
||||
<p className="text-3xl font-bold text-red-600 mt-1">
|
||||
{unresolvedCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<svg
|
||||
className="h-6 w-6 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2">Needs attention</p>
|
||||
</div>
|
||||
</AppLink>
|
||||
|
||||
<AppLink
|
||||
className="block"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_RESOLVED] as Route,
|
||||
)}
|
||||
>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Resolved</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-1">
|
||||
{resolvedCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-full bg-green-50 flex items-center justify-center">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2">Fixed and verified</p>
|
||||
</div>
|
||||
</AppLink>
|
||||
|
||||
<AppLink
|
||||
className="block"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_ARCHIVED] as Route,
|
||||
)}
|
||||
>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Archived</p>
|
||||
<p className="text-3xl font-bold text-gray-600 mt-1">
|
||||
{archivedCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-full bg-gray-50 flex items-center justify-center">
|
||||
<svg
|
||||
className="h-6 w-6 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Dismissed or won{"'"}t fix
|
||||
</p>
|
||||
</div>
|
||||
</AppLink>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Most Frequent Exceptions */}
|
||||
{topExceptions.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Most Frequent Bugs
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Unresolved exceptions with the highest occurrence count
|
||||
</p>
|
||||
</div>
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
|
||||
)}
|
||||
>
|
||||
View all
|
||||
</AppLink>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="divide-y divide-gray-100">
|
||||
{topExceptions.map(
|
||||
(exception: TelemetryException, index: number) => {
|
||||
const maxOccurrences: number =
|
||||
topExceptions[0]?.occuranceCount || 1;
|
||||
const barWidth: number =
|
||||
((exception.occuranceCount || 0) / maxOccurrences) * 100;
|
||||
|
||||
return (
|
||||
<AppLink
|
||||
key={exception.id?.toString() || index.toString()}
|
||||
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||
to={
|
||||
exception.fingerprint
|
||||
? new Route(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.EXCEPTIONS_VIEW_ROOT
|
||||
] as Route,
|
||||
)
|
||||
.toString()
|
||||
.replace(/\/$/, `/${exception.fingerprint}`),
|
||||
)
|
||||
: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.EXCEPTIONS_UNRESOLVED
|
||||
] as Route,
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<div className="min-w-0 flex-1 mr-3">
|
||||
<TelemetryExceptionElement
|
||||
message={
|
||||
exception.message ||
|
||||
exception.exceptionType ||
|
||||
"Unknown exception"
|
||||
}
|
||||
isResolved={exception.isResolved || false}
|
||||
isArchived={exception.isArchived || false}
|
||||
className="text-sm"
|
||||
/>
|
||||
<div className="flex items-center space-x-3 mt-1">
|
||||
{exception.service && (
|
||||
<span className="text-xs text-gray-500">
|
||||
{exception.service.name?.toString()}
|
||||
</span>
|
||||
)}
|
||||
{exception.environment && (
|
||||
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
|
||||
{exception.environment}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{(exception.occuranceCount || 0).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">occurrences</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="w-full h-1 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-red-400"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppLink>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Services Affected */}
|
||||
{serviceSummaries.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Affected Services
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Services with unresolved exceptions
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="divide-y divide-gray-100">
|
||||
{serviceSummaries.map((summary: ServiceExceptionSummary) => {
|
||||
return (
|
||||
<div
|
||||
key={summary.service.id?.toString()}
|
||||
className="px-4 py-4"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<TelemetryServiceElement
|
||||
telemetryService={summary.service}
|
||||
/>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold text-red-600">
|
||||
{summary.unresolvedCount}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">unresolved</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold text-gray-700">
|
||||
{summary.totalOccurrences.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">total hits</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExceptionsDashboard;
|
||||
@@ -0,0 +1,352 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import MetricType from "Common/Models/DatabaseModels/MetricType";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ServiceElement from "../Service/ServiceElement";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import AppLink from "../AppLink/AppLink";
|
||||
import Includes from "Common/Types/BaseDatabase/Includes";
|
||||
|
||||
interface ServiceMetricSummary {
|
||||
service: Service;
|
||||
metricCount: number;
|
||||
metricNames: Array<string>;
|
||||
}
|
||||
|
||||
const MetricsDashboard: FunctionComponent = (): ReactElement => {
|
||||
const [serviceSummaries, setServiceSummaries] = useState<
|
||||
Array<ServiceMetricSummary>
|
||||
>([]);
|
||||
const [totalMetricCount, setTotalMetricCount] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
// Load services
|
||||
const servicesResult: ListResult<Service> = await ModelAPI.getList({
|
||||
modelType: Service,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
select: {
|
||||
serviceColor: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
name: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
const services: Array<Service> = servicesResult.data || [];
|
||||
|
||||
// Load all metric types with their services
|
||||
const metricsResult: ListResult<MetricType> = await ModelAPI.getList({
|
||||
modelType: MetricType,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
unit: true,
|
||||
description: true,
|
||||
},
|
||||
relationSelect: {
|
||||
services: {
|
||||
_id: true,
|
||||
name: true,
|
||||
serviceColor: true,
|
||||
},
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
name: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
const metrics: Array<MetricType> = metricsResult.data || [];
|
||||
setTotalMetricCount(metrics.length);
|
||||
|
||||
// Build per-service summaries
|
||||
const summaryMap: Map<string, ServiceMetricSummary> = new Map();
|
||||
|
||||
for (const service of services) {
|
||||
const serviceId: string = service.id?.toString() || "";
|
||||
summaryMap.set(serviceId, {
|
||||
service,
|
||||
metricCount: 0,
|
||||
metricNames: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const metric of metrics) {
|
||||
const metricServices: Array<Service> = metric.services || [];
|
||||
|
||||
for (const metricService of metricServices) {
|
||||
const serviceId: string =
|
||||
metricService._id?.toString() ||
|
||||
metricService.id?.toString() ||
|
||||
"";
|
||||
let summary: ServiceMetricSummary | undefined =
|
||||
summaryMap.get(serviceId);
|
||||
|
||||
if (!summary) {
|
||||
// Service exists in metric but wasn't in our services list
|
||||
summary = {
|
||||
service: metricService,
|
||||
metricCount: 0,
|
||||
metricNames: [],
|
||||
};
|
||||
summaryMap.set(serviceId, summary);
|
||||
}
|
||||
|
||||
summary.metricCount += 1;
|
||||
|
||||
const metricName: string = metric.name || "";
|
||||
if (metricName && summary.metricNames.length < 5) {
|
||||
summary.metricNames.push(metricName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only show services that have metrics
|
||||
const summariesWithData: Array<ServiceMetricSummary> = Array.from(
|
||||
summaryMap.values(),
|
||||
).filter((s: ServiceMetricSummary) => {
|
||||
return s.metricCount > 0;
|
||||
});
|
||||
|
||||
// Sort by metric count descending
|
||||
summariesWithData.sort(
|
||||
(a: ServiceMetricSummary, b: ServiceMetricSummary) => {
|
||||
return b.metricCount - a.metricCount;
|
||||
},
|
||||
);
|
||||
|
||||
setServiceSummaries(summariesWithData);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err as Error));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDashboard();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
onRefreshClick={() => {
|
||||
void loadDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (serviceSummaries.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
|
||||
<div className="text-gray-400 text-5xl mb-4">
|
||||
<svg
|
||||
className="mx-auto h-16 w-16 text-indigo-200"
|
||||
fill="none"
|
||||
viewBox="0 0 48 48"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M6 38 L6 20 L12 20 L12 38"
|
||||
fill="currentColor"
|
||||
opacity={0.4}
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M16 38 L16 14 L22 14 L22 38"
|
||||
fill="currentColor"
|
||||
opacity={0.6}
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M26 38 L26 24 L32 24 L32 38"
|
||||
fill="currentColor"
|
||||
opacity={0.5}
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M36 38 L36 10 L42 10 L42 38"
|
||||
fill="currentColor"
|
||||
opacity={0.8}
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M4 38 L44 38"
|
||||
opacity={0.3}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No metrics data yet
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 max-w-md mx-auto">
|
||||
Once your services start sending metrics via OpenTelemetry, you{"'"}ll
|
||||
see a summary of which services are reporting, what metrics they
|
||||
collect, and more.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<p className="text-sm text-gray-500">Total Metrics</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||||
{totalMetricCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<p className="text-sm text-gray-500">Services Reporting</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||||
{serviceSummaries.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<p className="text-sm text-gray-500">Avg Metrics per Service</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||||
{serviceSummaries.length > 0
|
||||
? Math.round(totalMetricCount / serviceSummaries.length)
|
||||
: 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Cards */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Services Reporting Metrics
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Each service and the metrics it collects
|
||||
</p>
|
||||
</div>
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.METRICS_LIST] as Route,
|
||||
)}
|
||||
>
|
||||
View all metrics
|
||||
</AppLink>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{serviceSummaries.map((summary: ServiceMetricSummary) => {
|
||||
return (
|
||||
<div
|
||||
key={summary.service.id?.toString() || summary.service._id?.toString()}
|
||||
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<ServiceElement service={summary.service} />
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-gray-500">Metrics Collected</p>
|
||||
<p className="text-lg font-semibold text-gray-900">
|
||||
{summary.metricCount}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1.5">Sample Metrics</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{summary.metricNames.map((name: string) => {
|
||||
return (
|
||||
<span
|
||||
key={name}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{summary.metricCount > summary.metricNames.length && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
||||
+{summary.metricCount - summary.metricNames.length} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SERVICE_VIEW_METRICS] as Route,
|
||||
{
|
||||
modelId: new ObjectID(
|
||||
(summary.service._id as string) ||
|
||||
summary.service.id?.toString() ||
|
||||
"",
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
View service metrics
|
||||
</AppLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsDashboard;
|
||||
@@ -43,9 +43,9 @@ const MetricsTable: FunctionComponent<ComponentProps> = (
|
||||
sortBy="name"
|
||||
sortOrder={SortOrder.Ascending}
|
||||
cardProps={{
|
||||
title: "Metrics",
|
||||
title: "All Metrics",
|
||||
description:
|
||||
"Metrics are the individual data points that make up a service. They are the building blocks of a service and represent the work done by a single service.",
|
||||
"All metrics collected from your services. Click on a metric to explore its data in the chart viewer.",
|
||||
}}
|
||||
onViewPage={async (item: MetricType) => {
|
||||
const route: Route = RouteUtil.populateRouteParams(
|
||||
|
||||
@@ -108,7 +108,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: "Metrics",
|
||||
description: "Monitor system metrics.",
|
||||
description: "Monitor and visualize system metrics across your services.",
|
||||
route: RouteUtil.populateRouteParams(RouteMap[PageMap.METRICS] as Route),
|
||||
activeRoute: RouteMap[PageMap.METRICS],
|
||||
icon: IconProp.Heartbeat,
|
||||
@@ -117,7 +117,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: "Traces",
|
||||
description: "Distributed tracing analysis.",
|
||||
description: "Track requests across your services.",
|
||||
route: RouteUtil.populateRouteParams(RouteMap[PageMap.TRACES] as Route),
|
||||
activeRoute: RouteMap[PageMap.TRACES],
|
||||
icon: IconProp.Waterfall,
|
||||
@@ -125,8 +125,8 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
category: "Observability",
|
||||
},
|
||||
{
|
||||
title: "Profiles",
|
||||
description: "CPU and memory profiling.",
|
||||
title: "Performance Profiles",
|
||||
description: "Find slow functions and memory hotspots.",
|
||||
route: RouteUtil.populateRouteParams(RouteMap[PageMap.PROFILES] as Route),
|
||||
activeRoute: RouteMap[PageMap.PROFILES],
|
||||
icon: IconProp.Fire,
|
||||
@@ -135,7 +135,7 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
title: "Exceptions",
|
||||
description: "Catch and fix bugs early.",
|
||||
description: "Track and resolve bugs across your services.",
|
||||
route: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS] as Route,
|
||||
),
|
||||
|
||||
@@ -184,7 +184,8 @@ const DiffFlamegraph: FunctionComponent<DiffFlamegraphProps> = (
|
||||
) {
|
||||
return (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No profile data found in the selected time ranges.
|
||||
No performance data found in the selected time ranges. Try adjusting the
|
||||
time periods.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -323,14 +324,14 @@ const DiffFlamegraph: FunctionComponent<DiffFlamegraphProps> = (
|
||||
)}
|
||||
|
||||
<div className="mb-3 flex flex-wrap items-center space-x-4 text-xs text-gray-600">
|
||||
<span className="font-medium">Legend:</span>
|
||||
<span className="font-medium">What the colors mean:</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span className="inline-block w-3 h-3 rounded bg-red-500" />
|
||||
<span>Regression (slower)</span>
|
||||
<span>Got slower</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span className="inline-block w-3 h-3 rounded bg-green-500" />
|
||||
<span>Improvement (faster)</span>
|
||||
<span>Got faster</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span className="inline-block w-3 h-3 rounded bg-gray-400" />
|
||||
@@ -364,9 +365,9 @@ const DiffFlamegraph: FunctionComponent<DiffFlamegraphProps> = (
|
||||
<div className="text-gray-300">{tooltip.fileName}</div>
|
||||
)}
|
||||
<div className="mt-1">
|
||||
Baseline: {tooltip.baselineValue.toLocaleString()}
|
||||
Before: {tooltip.baselineValue.toLocaleString()}
|
||||
</div>
|
||||
<div>Comparison: {tooltip.comparisonValue.toLocaleString()}</div>
|
||||
<div>After: {tooltip.comparisonValue.toLocaleString()}</div>
|
||||
<div
|
||||
className={
|
||||
tooltip.delta > 0
|
||||
@@ -376,7 +377,7 @@ const DiffFlamegraph: FunctionComponent<DiffFlamegraphProps> = (
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Delta: {tooltip.delta > 0 ? "+" : ""}
|
||||
Change: {tooltip.delta > 0 ? "+" : ""}
|
||||
{tooltip.delta.toLocaleString()} (
|
||||
{tooltip.deltaPercent >= 0 ? "+" : ""}
|
||||
{tooltip.deltaPercent.toFixed(1)}%)
|
||||
|
||||
@@ -211,7 +211,8 @@ const ProfileFlamegraph: FunctionComponent<ProfileFlamegraphProps> = (
|
||||
if (samples.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No profile samples found for this profile.
|
||||
No performance data found for this profile. This can happen if the
|
||||
profile was recently captured and data is still being processed.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -325,19 +326,25 @@ const ProfileFlamegraph: FunctionComponent<ProfileFlamegraphProps> = (
|
||||
)}
|
||||
|
||||
<div className="mb-3 flex flex-wrap items-center space-x-4 text-xs text-gray-600">
|
||||
<span className="font-medium">Frame Types:</span>
|
||||
{["kernel", "native", "jvm", "cpython", "go", "v8js", "unknown"].map(
|
||||
(type: string) => {
|
||||
return (
|
||||
<span key={type} className="flex items-center space-x-1">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded ${ProfileUtil.getFrameTypeColor(type)}`}
|
||||
/>
|
||||
<span>{type}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<span className="font-medium">Code Type:</span>
|
||||
{[
|
||||
{ key: "kernel", label: "System / Kernel" },
|
||||
{ key: "native", label: "Native Code" },
|
||||
{ key: "jvm", label: "Java / JVM" },
|
||||
{ key: "cpython", label: "Python" },
|
||||
{ key: "go", label: "Go" },
|
||||
{ key: "v8js", label: "JavaScript" },
|
||||
{ key: "unknown", label: "Other" },
|
||||
].map((item: { key: string; label: string }) => {
|
||||
return (
|
||||
<span key={item.key} className="flex items-center space-x-1">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded ${ProfileUtil.getFrameTypeColor(item.key)}`}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -359,8 +366,10 @@ const ProfileFlamegraph: FunctionComponent<ProfileFlamegraphProps> = (
|
||||
{tooltip.fileName && (
|
||||
<div className="text-gray-300">{tooltip.fileName}</div>
|
||||
)}
|
||||
<div className="mt-1">Self: {tooltip.selfValue.toLocaleString()}</div>
|
||||
<div>Total: {tooltip.totalValue.toLocaleString()}</div>
|
||||
<div className="mt-1">
|
||||
Own Time: {tooltip.selfValue.toLocaleString()}
|
||||
</div>
|
||||
<div>Total Time: {tooltip.totalValue.toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,7 @@ const ProfileFunctionList: FunctionComponent<ProfileFunctionListProps> = (
|
||||
if (samples.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No profile samples found for this profile.
|
||||
No performance data found for this profile.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -228,7 +228,7 @@ const ProfileFunctionList: FunctionComponent<ProfileFunctionListProps> = (
|
||||
handleSort("fileName");
|
||||
}}
|
||||
>
|
||||
File{getSortIndicator("fileName")}
|
||||
Source File{getSortIndicator("fileName")}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right cursor-pointer hover:bg-gray-100 select-none"
|
||||
@@ -236,7 +236,7 @@ const ProfileFunctionList: FunctionComponent<ProfileFunctionListProps> = (
|
||||
handleSort("selfValue");
|
||||
}}
|
||||
>
|
||||
Self Value{getSortIndicator("selfValue")}
|
||||
Own Time{getSortIndicator("selfValue")}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right cursor-pointer hover:bg-gray-100 select-none"
|
||||
@@ -244,7 +244,7 @@ const ProfileFunctionList: FunctionComponent<ProfileFunctionListProps> = (
|
||||
handleSort("totalValue");
|
||||
}}
|
||||
>
|
||||
Total Value{getSortIndicator("totalValue")}
|
||||
Total Time{getSortIndicator("totalValue")}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right cursor-pointer hover:bg-gray-100 select-none"
|
||||
@@ -252,7 +252,7 @@ const ProfileFunctionList: FunctionComponent<ProfileFunctionListProps> = (
|
||||
handleSort("sampleCount");
|
||||
}}
|
||||
>
|
||||
Samples{getSortIndicator("sampleCount")}
|
||||
Occurrences{getSortIndicator("sampleCount")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -28,6 +28,7 @@ import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import ServiceElement from "../Service/ServiceElement";
|
||||
import ProfileUtil from "../../Utils/ProfileUtil";
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId?: ObjectID | undefined;
|
||||
@@ -196,23 +197,28 @@ const ProfileTable: FunctionComponent<ComponentProps> = (
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
singularName="Profile"
|
||||
pluralName="Profiles"
|
||||
name="Profiles"
|
||||
singularName="Performance Profile"
|
||||
pluralName="Performance Profiles"
|
||||
name="Performance Profiles"
|
||||
isViewable={true}
|
||||
cardProps={
|
||||
props.isMinimalTable
|
||||
? undefined
|
||||
: {
|
||||
title: "Profiles",
|
||||
title: "Performance Profiles",
|
||||
description:
|
||||
"Continuous profiling data from your services. Profiles help you understand CPU, memory, and allocation hotspots in your applications.",
|
||||
"See where your application spends the most time and memory. Use profiles to find slow functions and optimize performance.",
|
||||
}
|
||||
}
|
||||
query={query}
|
||||
selectMoreFields={{
|
||||
profileId: true,
|
||||
}}
|
||||
showViewIdButton={true}
|
||||
noItemsMessage={
|
||||
props.noItemsMessage ? props.noItemsMessage : "No profiles found."
|
||||
props.noItemsMessage
|
||||
? props.noItemsMessage
|
||||
: "No performance profiles found. Once your services start sending profiling data, they will appear here."
|
||||
}
|
||||
showRefreshButton={true}
|
||||
sortBy="startTime"
|
||||
@@ -245,7 +251,7 @@ const ProfileTable: FunctionComponent<ComponentProps> = (
|
||||
profileType: true,
|
||||
},
|
||||
type: FieldType.Text,
|
||||
title: "Profile Type",
|
||||
title: "Type",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
@@ -259,7 +265,7 @@ const ProfileTable: FunctionComponent<ComponentProps> = (
|
||||
startTime: true,
|
||||
},
|
||||
type: FieldType.DateTime,
|
||||
title: "Start Time",
|
||||
title: "Captured At",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
@@ -273,20 +279,6 @@ const ProfileTable: FunctionComponent<ComponentProps> = (
|
||||
]}
|
||||
onAdvancedFiltersToggle={handleAdvancedFiltersToggle}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
profileId: true,
|
||||
},
|
||||
title: "Profile ID",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
profileType: true,
|
||||
},
|
||||
title: "Profile Type",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
serviceId: true,
|
||||
@@ -312,18 +304,40 @@ const ProfileTable: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
profileType: true,
|
||||
},
|
||||
title: "Type",
|
||||
type: FieldType.Element,
|
||||
getElement: (profile: Profile): ReactElement => {
|
||||
const profileType: string = profile.profileType || "unknown";
|
||||
const displayName: string =
|
||||
ProfileUtil.getProfileTypeDisplayName(profileType);
|
||||
const badgeColor: string =
|
||||
ProfileUtil.getProfileTypeBadgeColor(profileType);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeColor}`}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
sampleCount: true,
|
||||
},
|
||||
title: "Samples",
|
||||
title: "Data Points",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
startTime: true,
|
||||
},
|
||||
title: "Start Time",
|
||||
title: "Captured At",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -168,7 +168,7 @@ const ProfileTimeline: FunctionComponent<ProfileTimelineProps> = (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-gray-600">
|
||||
Profile Density ({profiles.length} profiles)
|
||||
Activity ({profiles.length} profiles captured)
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{OneUptimeDate.getDateAsLocalFormattedString(props.startTime, true)} —{" "}
|
||||
|
||||
@@ -12,12 +12,12 @@ interface ProfileTypeOption {
|
||||
|
||||
const profileTypeOptions: Array<ProfileTypeOption> = [
|
||||
{ label: "All Types", value: undefined },
|
||||
{ label: "CPU", value: "cpu" },
|
||||
{ label: "Wall", value: "wall" },
|
||||
{ label: "Alloc Objects", value: "alloc_objects" },
|
||||
{ label: "Alloc Space", value: "alloc_space" },
|
||||
{ label: "Goroutine", value: "goroutine" },
|
||||
{ label: "Contention", value: "contention" },
|
||||
{ label: "CPU Usage", value: "cpu" },
|
||||
{ label: "Wall Clock Time", value: "wall" },
|
||||
{ label: "Memory Allocations (Count)", value: "alloc_objects" },
|
||||
{ label: "Memory Allocations (Size)", value: "alloc_space" },
|
||||
{ label: "Goroutines", value: "goroutine" },
|
||||
{ label: "Lock Contention", value: "contention" },
|
||||
];
|
||||
|
||||
const ProfileTypeSelector: FunctionComponent<ProfileTypeSelectorProps> = (
|
||||
@@ -25,7 +25,7 @@ const ProfileTypeSelector: FunctionComponent<ProfileTypeSelectorProps> = (
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-medium text-gray-700">Profile Type:</label>
|
||||
<label className="text-sm font-medium text-gray-700">Show:</label>
|
||||
<select
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
value={props.selectedProfileType || ""}
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import API from "Common/Utils/API";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import Profile from "Common/Models/AnalyticsModels/Profile";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult as AnalyticsListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ServiceElement from "../Service/ServiceElement";
|
||||
import ProfileUtil from "../../Utils/ProfileUtil";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import AppLink from "../AppLink/AppLink";
|
||||
|
||||
interface ServiceProfileSummary {
|
||||
service: Service;
|
||||
profileCount: number;
|
||||
latestProfileTime: Date | null;
|
||||
profileTypes: Array<string>;
|
||||
}
|
||||
|
||||
interface FunctionHotspot {
|
||||
functionName: string;
|
||||
fileName: string;
|
||||
selfValue: number;
|
||||
totalValue: number;
|
||||
sampleCount: number;
|
||||
frameType: string;
|
||||
}
|
||||
|
||||
const ProfilesDashboard: FunctionComponent = (): ReactElement => {
|
||||
const [serviceSummaries, setServiceSummaries] = useState<
|
||||
Array<ServiceProfileSummary>
|
||||
>([]);
|
||||
const [hotspots, setHotspots] = useState<Array<FunctionHotspot>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
const now: Date = OneUptimeDate.getCurrentDate();
|
||||
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
|
||||
|
||||
// Load services
|
||||
const servicesResult: ListResult<Service> = await ModelAPI.getList({
|
||||
modelType: Service,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
select: {
|
||||
serviceColor: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
name: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
const services: Array<Service> = servicesResult.data || [];
|
||||
|
||||
// Load recent profiles (last 1 hour) to build per-service summaries
|
||||
const profilesResult: AnalyticsListResult<Profile> =
|
||||
await AnalyticsModelAPI.getList({
|
||||
modelType: Profile,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
startTime: new InBetween(oneHourAgo, now),
|
||||
},
|
||||
select: {
|
||||
serviceId: true,
|
||||
profileType: true,
|
||||
startTime: true,
|
||||
sampleCount: true,
|
||||
},
|
||||
limit: 5000,
|
||||
skip: 0,
|
||||
sort: {
|
||||
startTime: SortOrder.Descending,
|
||||
},
|
||||
});
|
||||
|
||||
const profiles: Array<Profile> = profilesResult.data || [];
|
||||
|
||||
// Build per-service summaries
|
||||
const summaryMap: Map<string, ServiceProfileSummary> = new Map();
|
||||
|
||||
for (const service of services) {
|
||||
const serviceId: string = service.id?.toString() || "";
|
||||
summaryMap.set(serviceId, {
|
||||
service,
|
||||
profileCount: 0,
|
||||
latestProfileTime: null,
|
||||
profileTypes: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const profile of profiles) {
|
||||
const serviceId: string = profile.serviceId?.toString() || "";
|
||||
const summary: ServiceProfileSummary | undefined =
|
||||
summaryMap.get(serviceId);
|
||||
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
summary.profileCount += 1;
|
||||
|
||||
const profileTime: Date | undefined = profile.startTime
|
||||
? new Date(profile.startTime)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
profileTime &&
|
||||
(!summary.latestProfileTime ||
|
||||
profileTime > summary.latestProfileTime)
|
||||
) {
|
||||
summary.latestProfileTime = profileTime;
|
||||
}
|
||||
|
||||
const profileType: string = profile.profileType || "";
|
||||
|
||||
if (profileType && !summary.profileTypes.includes(profileType)) {
|
||||
summary.profileTypes.push(profileType);
|
||||
}
|
||||
}
|
||||
|
||||
// Only show services that have profiles
|
||||
const summariesWithData: Array<ServiceProfileSummary> = Array.from(
|
||||
summaryMap.values(),
|
||||
).filter((s: ServiceProfileSummary) => {
|
||||
return s.profileCount > 0;
|
||||
});
|
||||
|
||||
// Sort by profile count descending
|
||||
summariesWithData.sort(
|
||||
(a: ServiceProfileSummary, b: ServiceProfileSummary) => {
|
||||
return b.profileCount - a.profileCount;
|
||||
},
|
||||
);
|
||||
|
||||
setServiceSummaries(summariesWithData);
|
||||
|
||||
// Load top hotspots (function list) across all services
|
||||
try {
|
||||
const hotspotsResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/telemetry/profiles/function-list",
|
||||
),
|
||||
data: {
|
||||
startTime: oneHourAgo.toISOString(),
|
||||
endTime: now.toISOString(),
|
||||
limit: 10,
|
||||
sortBy: "selfValue",
|
||||
},
|
||||
headers: {
|
||||
...ModelAPI.getCommonHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (hotspotsResponse instanceof HTTPErrorResponse) {
|
||||
throw hotspotsResponse;
|
||||
}
|
||||
|
||||
const functions: Array<FunctionHotspot> = (hotspotsResponse.data[
|
||||
"functions"
|
||||
] || []) as unknown as Array<FunctionHotspot>;
|
||||
setHotspots(functions);
|
||||
} catch {
|
||||
// Hotspots are optional - don't fail the whole page
|
||||
setHotspots([]);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDashboard();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
onRefreshClick={() => {
|
||||
void loadDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (serviceSummaries.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
|
||||
<div className="text-gray-400 text-5xl mb-4">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No performance data yet
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 max-w-md mx-auto">
|
||||
Once your services start sending profiling data, you{"'"}ll see a
|
||||
summary of which services are being profiled, their performance
|
||||
hotspots, and more.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Service Cards */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Services Being Profiled
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Performance data collected in the last hour
|
||||
</p>
|
||||
</div>
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PROFILES_LIST] as Route,
|
||||
)}
|
||||
>
|
||||
View all profiles
|
||||
</AppLink>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{serviceSummaries.map((summary: ServiceProfileSummary) => {
|
||||
return (
|
||||
<div
|
||||
key={summary.service.id?.toString()}
|
||||
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<ServiceElement service={summary.service} />
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Profiles</p>
|
||||
<p className="text-lg font-semibold text-gray-900">
|
||||
{summary.profileCount}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Last Captured</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
{summary.latestProfileTime
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(
|
||||
summary.latestProfileTime,
|
||||
true,
|
||||
)
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1.5">
|
||||
Profile Types Collected
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{summary.profileTypes.map((profileType: string) => {
|
||||
const badgeColor: string =
|
||||
ProfileUtil.getProfileTypeBadgeColor(profileType);
|
||||
return (
|
||||
<span
|
||||
key={profileType}
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${badgeColor}`}
|
||||
>
|
||||
{ProfileUtil.getProfileTypeDisplayName(profileType)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SERVICE_VIEW_PROFILES] as Route,
|
||||
{
|
||||
modelId: new ObjectID(summary.service._id as string),
|
||||
},
|
||||
)}
|
||||
>
|
||||
View service profiles
|
||||
</AppLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Hotspots */}
|
||||
{hotspots.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Top Performance Hotspots
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Functions using the most resources across all services in the last
|
||||
hour
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-5 py-3 font-medium">#</th>
|
||||
<th className="px-5 py-3 font-medium">Function</th>
|
||||
<th className="px-5 py-3 font-medium">Source File</th>
|
||||
<th className="px-5 py-3 text-right font-medium">Own Time</th>
|
||||
<th className="px-5 py-3 text-right font-medium">
|
||||
Total Time
|
||||
</th>
|
||||
<th className="px-5 py-3 text-right font-medium">
|
||||
Occurrences
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hotspots.map((fn: FunctionHotspot, index: number) => {
|
||||
const maxSelf: number = hotspots[0]?.selfValue || 1;
|
||||
const barWidth: number = (fn.selfValue / maxSelf) * 100;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`${fn.functionName}-${fn.fileName}-${index}`}
|
||||
className="border-t border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
<td className="px-5 py-3 text-gray-400 font-mono text-xs">
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
<div className="font-mono text-xs text-gray-900 truncate max-w-xs">
|
||||
{fn.functionName}
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 h-1 rounded-full bg-orange-400"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-5 py-3 text-gray-500 text-xs truncate max-w-xs">
|
||||
{fn.fileName || "-"}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right font-mono text-xs text-gray-900">
|
||||
{fn.selfValue.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right font-mono text-xs text-gray-700">
|
||||
{fn.totalValue.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right font-mono text-xs text-gray-700">
|
||||
{fn.sampleCount.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilesDashboard;
|
||||
@@ -1728,7 +1728,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
|
||||
tokenValue,
|
||||
pyroscopeUrl,
|
||||
)}
|
||||
language="hcl"
|
||||
language="nginx"
|
||||
/>,
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import API from "Common/Utils/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import Span, { SpanStatus } from "Common/Models/AnalyticsModels/Span";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult as AnalyticsListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ServiceElement from "../Service/ServiceElement";
|
||||
import SpanStatusElement from "../Span/SpanStatusElement";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import AppLink from "../AppLink/AppLink";
|
||||
|
||||
interface ServiceTraceSummary {
|
||||
service: Service;
|
||||
totalTraces: number;
|
||||
errorTraces: number;
|
||||
latestTraceTime: Date | null;
|
||||
}
|
||||
|
||||
interface RecentTrace {
|
||||
traceId: string;
|
||||
name: string;
|
||||
serviceId: string;
|
||||
startTime: Date;
|
||||
statusCode: SpanStatus;
|
||||
durationNano: number;
|
||||
}
|
||||
|
||||
const TracesDashboard: FunctionComponent = (): ReactElement => {
|
||||
const [serviceSummaries, setServiceSummaries] = useState<
|
||||
Array<ServiceTraceSummary>
|
||||
>([]);
|
||||
const [recentErrorTraces, setRecentErrorTraces] = useState<
|
||||
Array<RecentTrace>
|
||||
>([]);
|
||||
const [recentSlowTraces, setRecentSlowTraces] = useState<Array<RecentTrace>>(
|
||||
[],
|
||||
);
|
||||
const [services, setServices] = useState<Array<Service>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const formatDuration: (nanos: number) => string = (nanos: number): string => {
|
||||
if (nanos >= 1_000_000_000) {
|
||||
return `${(nanos / 1_000_000_000).toFixed(2)}s`;
|
||||
}
|
||||
if (nanos >= 1_000_000) {
|
||||
return `${(nanos / 1_000_000).toFixed(1)}ms`;
|
||||
}
|
||||
if (nanos >= 1_000) {
|
||||
return `${(nanos / 1_000).toFixed(0)}us`;
|
||||
}
|
||||
return `${nanos}ns`;
|
||||
};
|
||||
|
||||
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
const now: Date = OneUptimeDate.getCurrentDate();
|
||||
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
|
||||
|
||||
// Load services
|
||||
const servicesResult: ListResult<Service> = await ModelAPI.getList({
|
||||
modelType: Service,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
select: {
|
||||
serviceColor: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
sort: {
|
||||
name: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
const loadedServices: Array<Service> = servicesResult.data || [];
|
||||
setServices(loadedServices);
|
||||
|
||||
// Load recent spans (last 1 hour) to build per-service summaries
|
||||
const spansResult: AnalyticsListResult<Span> =
|
||||
await AnalyticsModelAPI.getList({
|
||||
modelType: Span,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
startTime: new InBetween(oneHourAgo, now),
|
||||
},
|
||||
select: {
|
||||
traceId: true,
|
||||
spanId: true,
|
||||
parentSpanId: true,
|
||||
serviceId: true,
|
||||
name: true,
|
||||
startTime: true,
|
||||
statusCode: true,
|
||||
durationUnixNano: true,
|
||||
},
|
||||
limit: 5000,
|
||||
skip: 0,
|
||||
sort: {
|
||||
startTime: SortOrder.Descending,
|
||||
},
|
||||
});
|
||||
|
||||
const allSpans: Array<Span> = spansResult.data || [];
|
||||
|
||||
// Build per-service summaries from all spans
|
||||
const summaryMap: Map<string, ServiceTraceSummary> = new Map();
|
||||
|
||||
for (const service of loadedServices) {
|
||||
const serviceId: string = service.id?.toString() || "";
|
||||
summaryMap.set(serviceId, {
|
||||
service,
|
||||
totalTraces: 0,
|
||||
errorTraces: 0,
|
||||
latestTraceTime: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Track unique traces per service for counting
|
||||
const serviceTraceIds: Map<string, Set<string>> = new Map();
|
||||
const serviceErrorTraceIds: Map<string, Set<string>> = new Map();
|
||||
|
||||
const errorTraces: Array<RecentTrace> = [];
|
||||
const allTraces: Array<RecentTrace> = [];
|
||||
const seenTraceIds: Set<string> = new Set();
|
||||
const seenErrorTraceIds: Set<string> = new Set();
|
||||
|
||||
for (const span of allSpans) {
|
||||
const serviceId: string = span.serviceId?.toString() || "";
|
||||
const traceId: string = span.traceId?.toString() || "";
|
||||
const summary: ServiceTraceSummary | undefined =
|
||||
summaryMap.get(serviceId);
|
||||
|
||||
if (summary) {
|
||||
// Count unique traces per service
|
||||
if (!serviceTraceIds.has(serviceId)) {
|
||||
serviceTraceIds.set(serviceId, new Set());
|
||||
}
|
||||
|
||||
if (!serviceErrorTraceIds.has(serviceId)) {
|
||||
serviceErrorTraceIds.set(serviceId, new Set());
|
||||
}
|
||||
|
||||
const traceSet: Set<string> = serviceTraceIds.get(serviceId)!;
|
||||
|
||||
if (!traceSet.has(traceId)) {
|
||||
traceSet.add(traceId);
|
||||
summary.totalTraces += 1;
|
||||
}
|
||||
|
||||
if (span.statusCode === SpanStatus.Error) {
|
||||
const errorSet: Set<string> =
|
||||
serviceErrorTraceIds.get(serviceId)!;
|
||||
|
||||
if (!errorSet.has(traceId)) {
|
||||
errorSet.add(traceId);
|
||||
summary.errorTraces += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const spanTime: Date | undefined = span.startTime
|
||||
? new Date(span.startTime)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
spanTime &&
|
||||
(!summary.latestTraceTime || spanTime > summary.latestTraceTime)
|
||||
) {
|
||||
summary.latestTraceTime = spanTime;
|
||||
}
|
||||
}
|
||||
|
||||
// For the recent traces lists, pick the first span per trace
|
||||
// (which is the most recent since we sort desc)
|
||||
if (!seenTraceIds.has(traceId) && traceId) {
|
||||
seenTraceIds.add(traceId);
|
||||
|
||||
const traceRecord: RecentTrace = {
|
||||
traceId: traceId,
|
||||
name: span.name?.toString() || "Unknown",
|
||||
serviceId: serviceId,
|
||||
startTime: span.startTime
|
||||
? new Date(span.startTime)
|
||||
: new Date(),
|
||||
statusCode: span.statusCode || SpanStatus.Unset,
|
||||
durationNano: (span.durationUnixNano as number) || 0,
|
||||
};
|
||||
|
||||
allTraces.push(traceRecord);
|
||||
}
|
||||
|
||||
// Collect error spans, deduped by trace
|
||||
if (
|
||||
span.statusCode === SpanStatus.Error &&
|
||||
traceId &&
|
||||
!seenErrorTraceIds.has(traceId)
|
||||
) {
|
||||
seenErrorTraceIds.add(traceId);
|
||||
|
||||
errorTraces.push({
|
||||
traceId: traceId,
|
||||
name: span.name?.toString() || "Unknown",
|
||||
serviceId: serviceId,
|
||||
startTime: span.startTime
|
||||
? new Date(span.startTime)
|
||||
: new Date(),
|
||||
statusCode: span.statusCode,
|
||||
durationNano: (span.durationUnixNano as number) || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only show services that have traces
|
||||
const summariesWithData: Array<ServiceTraceSummary> = Array.from(
|
||||
summaryMap.values(),
|
||||
).filter((s: ServiceTraceSummary) => {
|
||||
return s.totalTraces > 0;
|
||||
});
|
||||
|
||||
// Sort by total traces descending
|
||||
summariesWithData.sort(
|
||||
(a: ServiceTraceSummary, b: ServiceTraceSummary) => {
|
||||
return b.totalTraces - a.totalTraces;
|
||||
},
|
||||
);
|
||||
|
||||
setServiceSummaries(summariesWithData);
|
||||
setRecentErrorTraces(errorTraces.slice(0, 10));
|
||||
|
||||
// Get slowest traces
|
||||
const slowTraces: Array<RecentTrace> = [...allTraces]
|
||||
.sort((a: RecentTrace, b: RecentTrace) => {
|
||||
return b.durationNano - a.durationNano;
|
||||
})
|
||||
.slice(0, 10);
|
||||
setRecentSlowTraces(slowTraces);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDashboard();
|
||||
}, []);
|
||||
|
||||
const getServiceName: (serviceId: string) => string = (
|
||||
serviceId: string,
|
||||
): string => {
|
||||
const service: Service | undefined = services.find((s: Service) => {
|
||||
return s.id?.toString() === serviceId;
|
||||
});
|
||||
return service?.name?.toString() || "Unknown";
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
onRefreshClick={() => {
|
||||
void loadDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (serviceSummaries.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
|
||||
<div className="mb-4">
|
||||
<svg
|
||||
className="mx-auto h-16 w-16 text-indigo-200"
|
||||
fill="none"
|
||||
viewBox="0 0 48 48"
|
||||
stroke="currentColor"
|
||||
>
|
||||
{/* Three horizontal bars representing a waterfall/trace timeline */}
|
||||
<rect x="4" y="10" width="28" height="5" rx="2.5" strokeWidth={1.5} fill="currentColor" opacity={0.5} />
|
||||
<rect x="12" y="20" width="20" height="5" rx="2.5" strokeWidth={1.5} fill="currentColor" opacity={0.7} />
|
||||
<rect x="20" y="30" width="24" height="5" rx="2.5" strokeWidth={1.5} fill="currentColor" opacity={0.9} />
|
||||
{/* Connecting lines */}
|
||||
<path d="M18 15 L16 20" strokeWidth={1.5} opacity={0.4} />
|
||||
<path d="M22 25 L24 30" strokeWidth={1.5} opacity={0.4} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No trace data yet
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 max-w-md mx-auto">
|
||||
Once your services start sending distributed tracing data, you{"'"}ll
|
||||
see a summary of requests flowing through your system, error rates,
|
||||
and slow operations.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Service Cards */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Services Overview
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Request activity across your services in the last hour
|
||||
</p>
|
||||
</div>
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACES_LIST] as Route,
|
||||
)}
|
||||
>
|
||||
View all spans
|
||||
</AppLink>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{serviceSummaries.map((summary: ServiceTraceSummary) => {
|
||||
const errorRate: number =
|
||||
summary.totalTraces > 0
|
||||
? (summary.errorTraces / summary.totalTraces) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={summary.service.id?.toString()}
|
||||
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<ServiceElement service={summary.service} />
|
||||
{errorRate > 5 ? (
|
||||
<span className="text-xs bg-red-100 text-red-800 px-2 py-0.5 rounded-full font-medium">
|
||||
{errorRate.toFixed(1)}% errors
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
|
||||
Healthy
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 mb-3">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Requests</p>
|
||||
<p className="text-lg font-semibold text-gray-900">
|
||||
{summary.totalTraces.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Errors</p>
|
||||
<p
|
||||
className={`text-lg font-semibold ${summary.errorTraces > 0 ? "text-red-600" : "text-gray-900"}`}
|
||||
>
|
||||
{summary.errorTraces.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Last Seen</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
{summary.latestTraceTime
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(
|
||||
summary.latestTraceTime,
|
||||
true,
|
||||
)
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error rate bar */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
|
||||
<span>Error Rate</span>
|
||||
<span>{errorRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${errorRate > 5 ? "bg-red-500" : errorRate > 0 ? "bg-yellow-400" : "bg-green-400"}`}
|
||||
style={{
|
||||
width: `${Math.max(errorRate, errorRate > 0 ? 2 : 0)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<AppLink
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SERVICE_VIEW_TRACES] as Route,
|
||||
{
|
||||
modelId: new ObjectID(summary.service._id as string),
|
||||
},
|
||||
)}
|
||||
>
|
||||
View service traces
|
||||
</AppLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two-column layout for errors and slow traces */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Recent Errors */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Recent Errors
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Failed requests in the last hour
|
||||
</p>
|
||||
</div>
|
||||
{recentErrorTraces.length === 0 ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-8 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
No errors in the last hour
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="divide-y divide-gray-100">
|
||||
{recentErrorTraces.map((trace: RecentTrace, index: number) => {
|
||||
return (
|
||||
<AppLink
|
||||
key={`${trace.traceId}-${index}`}
|
||||
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACE_VIEW]!,
|
||||
{
|
||||
modelId: trace.traceId,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3 min-w-0">
|
||||
<SpanStatusElement
|
||||
spanStatusCode={trace.statusCode}
|
||||
title=""
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{trace.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{getServiceName(trace.serviceId)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-3">
|
||||
<p className="text-xs font-mono text-gray-600">
|
||||
{formatDuration(trace.durationNano)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{OneUptimeDate.getDateAsLocalFormattedString(
|
||||
trace.startTime,
|
||||
true,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AppLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Slowest Traces */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Slowest Requests
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Longest running operations in the last hour
|
||||
</p>
|
||||
</div>
|
||||
{recentSlowTraces.length === 0 ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-8 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
No traces found in the last hour
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="divide-y divide-gray-100">
|
||||
{recentSlowTraces.map((trace: RecentTrace, index: number) => {
|
||||
const maxDuration: number =
|
||||
recentSlowTraces[0]?.durationNano || 1;
|
||||
const barWidth: number =
|
||||
(trace.durationNano / maxDuration) * 100;
|
||||
|
||||
return (
|
||||
<AppLink
|
||||
key={`${trace.traceId}-slow-${index}`}
|
||||
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACE_VIEW]!,
|
||||
{
|
||||
modelId: trace.traceId,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center space-x-3 min-w-0">
|
||||
<SpanStatusElement
|
||||
spanStatusCode={trace.statusCode}
|
||||
title=""
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{trace.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{getServiceName(trace.serviceId)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-3">
|
||||
<p className="text-sm font-mono font-semibold text-gray-900">
|
||||
{formatDuration(trace.durationNano)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-8">
|
||||
<div className="w-full h-1 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-amber-400"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default TracesDashboard;
|
||||
@@ -15,7 +15,7 @@ const ExceptionsLayout: FunctionComponent<
|
||||
|
||||
if (path.endsWith("exceptions") || path.endsWith("exceptions/*")) {
|
||||
Navigation.navigate(
|
||||
RouteUtil.populateRouteParams(RouteMap[PageMap.EXCEPTIONS_UNRESOLVED]!),
|
||||
RouteUtil.populateRouteParams(RouteMap[PageMap.EXCEPTIONS_OVERVIEW]!),
|
||||
);
|
||||
|
||||
return <></>;
|
||||
|
||||
68
App/FeatureSet/Dashboard/src/Pages/Exceptions/Overview.tsx
Normal file
68
App/FeatureSet/Dashboard/src/Pages/Exceptions/Overview.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionsDashboard from "../../Components/Exceptions/ExceptionsDashboard";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const ExceptionsOverviewPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
): ReactElement => {
|
||||
const disableTelemetryForThisProject: boolean =
|
||||
props.currentProject?.reseller?.enableTelemetryFeatures === false;
|
||||
|
||||
const [serviceCount, setServiceCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: Service,
|
||||
query: {},
|
||||
});
|
||||
setServiceCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (disableTelemetryForThisProject) {
|
||||
return (
|
||||
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (serviceCount === 0) {
|
||||
return <TelemetryDocumentation telemetryType="exceptions" />;
|
||||
}
|
||||
|
||||
return <ExceptionsDashboard />;
|
||||
};
|
||||
|
||||
export default ExceptionsOverviewPage;
|
||||
@@ -15,6 +15,15 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
{
|
||||
title: "Exceptions",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_OVERVIEW] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Home,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "Unresolved",
|
||||
@@ -52,11 +61,11 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
title: "Help",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
title: "Setup Guide",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route,
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ const ExceptionViewLayout: FunctionComponent<
|
||||
|
||||
if (path.endsWith("exceptions")) {
|
||||
Navigation.navigate(
|
||||
RouteUtil.populateRouteParams(RouteMap[PageMap.EXCEPTIONS_UNRESOLVED]!),
|
||||
RouteUtil.populateRouteParams(RouteMap[PageMap.EXCEPTIONS_OVERVIEW]!),
|
||||
);
|
||||
|
||||
return <></>;
|
||||
@@ -22,7 +22,7 @@ const ExceptionViewLayout: FunctionComponent<
|
||||
|
||||
return (
|
||||
<Page
|
||||
title="Exception Explorer"
|
||||
title="Exception Details"
|
||||
breadcrumbLinks={getExceptionsBreadcrumbs(path)}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import MetricsTable from "../../Components/Metrics/MetricsTable";
|
||||
import MetricsDashboard from "../../Components/Metrics/MetricsDashboard";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
@@ -62,7 +62,7 @@ const MetricsPage: FunctionComponent<PageComponentProps> = (
|
||||
return <TelemetryDocumentation telemetryType="metrics" />;
|
||||
}
|
||||
|
||||
return <MetricsTable />;
|
||||
return <MetricsDashboard />;
|
||||
};
|
||||
|
||||
export default MetricsPage;
|
||||
|
||||
8
App/FeatureSet/Dashboard/src/Pages/Metrics/List.tsx
Normal file
8
App/FeatureSet/Dashboard/src/Pages/Metrics/List.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import MetricsTable from "../../Components/Metrics/MetricsTable";
|
||||
|
||||
const MetricsListPage: FunctionComponent = (): ReactElement => {
|
||||
return <MetricsTable />;
|
||||
};
|
||||
|
||||
export default MetricsListPage;
|
||||
@@ -14,21 +14,30 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "All Metrics",
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.METRICS] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Home,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "All Metrics",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.METRICS_LIST] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.ChartBar,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
title: "Help",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
title: "Setup Guide",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.METRICS_DOCUMENTATION] as Route,
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@ const MetricsViewLayout: FunctionComponent<
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<Page
|
||||
title="Metrics Explorer"
|
||||
title="Metric Explorer"
|
||||
breadcrumbLinks={getMetricsBreadcrumbs(path)}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@@ -12,7 +12,7 @@ import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ProfileTable from "../../Components/Profiles/ProfileTable";
|
||||
import ProfilesDashboard from "../../Components/Profiles/ProfilesDashboard";
|
||||
|
||||
const ProfilesPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -62,7 +62,7 @@ const ProfilesPage: FunctionComponent<PageComponentProps> = (
|
||||
return <TelemetryDocumentation telemetryType="profiles" />;
|
||||
}
|
||||
|
||||
return <ProfileTable />;
|
||||
return <ProfilesDashboard />;
|
||||
};
|
||||
|
||||
export default ProfilesPage;
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProfilesLayout: FunctionComponent<
|
||||
|
||||
return (
|
||||
<Page
|
||||
title="Profiles"
|
||||
title="Performance Profiles"
|
||||
breadcrumbLinks={getProfilesBreadcrumbs(path)}
|
||||
sideMenu={<SideMenu />}
|
||||
>
|
||||
|
||||
11
App/FeatureSet/Dashboard/src/Pages/Profiles/List.tsx
Normal file
11
App/FeatureSet/Dashboard/src/Pages/Profiles/List.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import ProfileTable from "../../Components/Profiles/ProfileTable";
|
||||
|
||||
const ProfilesListPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return <ProfileTable />;
|
||||
};
|
||||
|
||||
export default ProfilesListPage;
|
||||
@@ -10,13 +10,22 @@ import React, { FunctionComponent, ReactElement } from "react";
|
||||
const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
const sections: SideMenuSectionProps[] = [
|
||||
{
|
||||
title: "Profiles",
|
||||
title: "Performance",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PROFILES] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Home,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "All Profiles",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PROFILES] as Route,
|
||||
RouteMap[PageMap.PROFILES_LIST] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Fire,
|
||||
@@ -24,11 +33,11 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
title: "Help",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
title: "Setup Guide",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PROFILES_DOCUMENTATION] as Route,
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ const ProfileViewPage: FunctionComponent<
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Flamegraph",
|
||||
name: "Performance Map",
|
||||
children: (
|
||||
<ProfileFlamegraph
|
||||
profileId={profileId}
|
||||
@@ -32,7 +32,7 @@ const ProfileViewPage: FunctionComponent<
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Function List",
|
||||
name: "Hotspots",
|
||||
children: (
|
||||
<ProfileFunctionList
|
||||
profileId={profileId}
|
||||
@@ -41,12 +41,13 @@ const ProfileViewPage: FunctionComponent<
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Diff",
|
||||
name: "Compare",
|
||||
children: (
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Compare profile data between two time ranges. Baseline is the
|
||||
earlier period, comparison is the more recent period.
|
||||
Compare performance between two time periods to see what got faster
|
||||
or slower. The baseline is the earlier period, and the comparison is
|
||||
the more recent period.
|
||||
</p>
|
||||
<DiffFlamegraph
|
||||
baselineStartTime={twoHoursAgo}
|
||||
|
||||
@@ -12,7 +12,7 @@ const ProfilesViewLayout: FunctionComponent<
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<Page
|
||||
title="Profile Explorer"
|
||||
title="Profile Details"
|
||||
breadcrumbLinks={getProfilesBreadcrumbs(path)}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import ExceptionsTable from "../../../Components/Exceptions/ExceptionsTable";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const ServiceExceptions: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ExceptionsTable
|
||||
serviceId={modelId}
|
||||
query={{}}
|
||||
title="Exceptions"
|
||||
description="All the exceptions for this service."
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceExceptions;
|
||||
@@ -136,7 +136,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Profiles",
|
||||
title: "Performance Profiles",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SERVICE_VIEW_PROFILES] as Route,
|
||||
{ modelId: props.modelId },
|
||||
@@ -144,6 +144,17 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Fire}
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Exceptions",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SERVICE_VIEW_EXCEPTIONS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Error}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import TraceTable from "../../Components/Traces/TraceTable";
|
||||
import TracesDashboard from "../../Components/Traces/TracesDashboard";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
@@ -62,7 +62,7 @@ const TracesPage: FunctionComponent<PageComponentProps> = (
|
||||
return <TelemetryDocumentation telemetryType="traces" />;
|
||||
}
|
||||
|
||||
return <TraceTable />;
|
||||
return <TracesDashboard />;
|
||||
};
|
||||
|
||||
export default TracesPage;
|
||||
|
||||
11
App/FeatureSet/Dashboard/src/Pages/Traces/List.tsx
Normal file
11
App/FeatureSet/Dashboard/src/Pages/Traces/List.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TraceTable from "../../Components/Traces/TraceTable";
|
||||
|
||||
const TracesListPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return <TraceTable />;
|
||||
};
|
||||
|
||||
export default TracesListPage;
|
||||
@@ -14,21 +14,30 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "All Traces",
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACES] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Home,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "All Spans",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACES_LIST] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.RectangleStack,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
title: "Help",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
title: "Setup Guide",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACES_DOCUMENTATION] as Route,
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ const TracesViewLayout: FunctionComponent<
|
||||
> = (): ReactElement => {
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<Page title="Trace Explorer" breadcrumbLinks={getTracesBreadcrumbs(path)}>
|
||||
<Page title="Trace Details" breadcrumbLinks={getTracesBreadcrumbs(path)}>
|
||||
<Outlet />
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import ExceptionsOverview from "../Pages/Exceptions/Overview";
|
||||
import ExceptionsUnresolved from "../Pages/Exceptions/Unresolved";
|
||||
import ExceptionsResolved from "../Pages/Exceptions/Resolved";
|
||||
import ExceptionsArchived from "../Pages/Exceptions/Archived";
|
||||
@@ -23,13 +24,23 @@ const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
|
||||
<PageRoute
|
||||
index
|
||||
element={
|
||||
<ExceptionsUnresolved
|
||||
<ExceptionsOverview
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.EXCEPTIONS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={ExceptionsRoutePath[PageMap.EXCEPTIONS_OVERVIEW] || ""}
|
||||
element={
|
||||
<ExceptionsOverview
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.EXCEPTIONS_OVERVIEW] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={ExceptionsRoutePath[PageMap.EXCEPTIONS_UNRESOLVED] || ""}
|
||||
element={
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import MetricsPage from "../Pages/Metrics/Index";
|
||||
import MetricsListPage from "../Pages/Metrics/List";
|
||||
import MetricsDocumentationPage from "../Pages/Metrics/Documentation";
|
||||
|
||||
import MetricViewPage from "../Pages/Metrics/View/Index";
|
||||
@@ -28,6 +29,12 @@ const MetricsRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={MetricsRoutePath[PageMap.METRICS_LIST] || ""}
|
||||
element={
|
||||
<MetricsListPage />
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={MetricsRoutePath[PageMap.METRICS_DOCUMENTATION] || ""}
|
||||
element={
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import ProfilesPage from "../Pages/Profiles/Index";
|
||||
import ProfilesListPage from "../Pages/Profiles/List";
|
||||
import ProfilesDocumentationPage from "../Pages/Profiles/Documentation";
|
||||
import ProfileViewPage from "../Pages/Profiles/View/Index";
|
||||
|
||||
@@ -27,6 +28,15 @@ const ProfilesRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={ProfilesRoutePath[PageMap.PROFILES_LIST] || ""}
|
||||
element={
|
||||
<ProfilesListPage
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.PROFILES_LIST] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={ProfilesRoutePath[PageMap.PROFILES_DOCUMENTATION] || ""}
|
||||
element={
|
||||
|
||||
@@ -26,6 +26,8 @@ import ServiceViewMetrics from "../Pages/Service/View/Metrics";
|
||||
|
||||
import ServiceViewProfiles from "../Pages/Service/View/Profiles";
|
||||
|
||||
import ServiceViewExceptions from "../Pages/Service/View/Exceptions";
|
||||
|
||||
import ServiceViewDelete from "../Pages/Service/View/Delete";
|
||||
|
||||
import ServiceViewSettings from "../Pages/Service/View/Settings";
|
||||
@@ -166,6 +168,16 @@ const ServiceRoutes: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SERVICE_VIEW_EXCEPTIONS)}
|
||||
element={
|
||||
<ServiceViewExceptions
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.SERVICE_VIEW_EXCEPTIONS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SERVICE_VIEW_OWNERS)}
|
||||
element={
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import TracesPage from "../Pages/Traces/Index";
|
||||
import TracesListPage from "../Pages/Traces/List";
|
||||
import TracesDocumentationPage from "../Pages/Traces/Documentation";
|
||||
|
||||
import TraceViewPage from "../Pages/Traces/View/Index";
|
||||
@@ -28,6 +29,15 @@ const TracesRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={TracesRoutePath[PageMap.TRACES_LIST] || ""}
|
||||
element={
|
||||
<TracesListPage
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.TRACES_LIST] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={TracesRoutePath[PageMap.TRACES_DOCUMENTATION] || ""}
|
||||
element={
|
||||
|
||||
@@ -11,6 +11,11 @@ export function getExceptionsBreadcrumbs(
|
||||
"Project",
|
||||
"Exceptions",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_OVERVIEW, [
|
||||
"Project",
|
||||
"Exceptions",
|
||||
"Overview",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_UNRESOLVED, [
|
||||
"Project",
|
||||
"Exceptions",
|
||||
@@ -29,12 +34,12 @@ export function getExceptionsBreadcrumbs(
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_VIEW, [
|
||||
"Project",
|
||||
"Exceptions",
|
||||
"View Exception",
|
||||
"Exception Details",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Exceptions",
|
||||
"Documentation",
|
||||
"Setup Guide",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
|
||||
@@ -6,15 +6,20 @@ import Link from "Common/Types/Link";
|
||||
export function getMetricsBreadcrumbs(path: string): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.METRICS, ["Project", "Metrics"]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.METRICS_LIST, [
|
||||
"Project",
|
||||
"Metrics",
|
||||
"All Metrics",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.METRIC_VIEW, [
|
||||
"Project",
|
||||
"Metrics",
|
||||
"Metrics Explorer",
|
||||
"Metric Explorer",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.METRICS_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Metrics",
|
||||
"Documentation",
|
||||
"Setup Guide",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
|
||||
@@ -5,16 +5,24 @@ import Link from "Common/Types/Link";
|
||||
|
||||
export function getProfilesBreadcrumbs(path: string): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES, ["Project", "Profiles"]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES, [
|
||||
"Project",
|
||||
"Performance Profiles",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES_LIST, [
|
||||
"Project",
|
||||
"Performance Profiles",
|
||||
"All Profiles",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.PROFILE_VIEW, [
|
||||
"Project",
|
||||
"Profiles",
|
||||
"Profile Explorer",
|
||||
"Performance Profiles",
|
||||
"Profile Details",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Profiles",
|
||||
"Documentation",
|
||||
"Performance Profiles",
|
||||
"Setup Guide",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
|
||||
@@ -6,15 +6,20 @@ import Link from "Common/Types/Link";
|
||||
export function getTracesBreadcrumbs(path: string): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.TRACES, ["Project", "Traces"]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.TRACES_LIST, [
|
||||
"Project",
|
||||
"Traces",
|
||||
"All Spans",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.TRACE_VIEW, [
|
||||
"Project",
|
||||
"Traces",
|
||||
"Trace Explorer",
|
||||
"Trace Details",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.TRACES_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Traces",
|
||||
"Documentation",
|
||||
"Setup Guide",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
|
||||
@@ -15,18 +15,21 @@ enum PageMap {
|
||||
// Metrics (standalone product)
|
||||
METRICS_ROOT = "METRICS_ROOT",
|
||||
METRICS = "METRICS",
|
||||
METRICS_LIST = "METRICS_LIST",
|
||||
METRIC_VIEW = "METRIC_VIEW",
|
||||
METRICS_DOCUMENTATION = "METRICS_DOCUMENTATION",
|
||||
|
||||
// Traces (standalone product)
|
||||
TRACES_ROOT = "TRACES_ROOT",
|
||||
TRACES = "TRACES",
|
||||
TRACES_LIST = "TRACES_LIST",
|
||||
TRACE_VIEW = "TRACE_VIEW",
|
||||
TRACES_DOCUMENTATION = "TRACES_DOCUMENTATION",
|
||||
|
||||
// Profiles (standalone product)
|
||||
PROFILES_ROOT = "PROFILES_ROOT",
|
||||
PROFILES = "PROFILES",
|
||||
PROFILES_LIST = "PROFILES_LIST",
|
||||
PROFILE_VIEW = "PROFILE_VIEW",
|
||||
PROFILES_DOCUMENTATION = "PROFILES_DOCUMENTATION",
|
||||
|
||||
@@ -218,6 +221,7 @@ enum PageMap {
|
||||
SERVICE_VIEW_TRACES = "SERVICE_VIEW_TRACES",
|
||||
SERVICE_VIEW_METRICS = "SERVICE_VIEW_METRICS",
|
||||
SERVICE_VIEW_PROFILES = "SERVICE_VIEW_PROFILES",
|
||||
SERVICE_VIEW_EXCEPTIONS = "SERVICE_VIEW_EXCEPTIONS",
|
||||
SERVICE_VIEW_OWNERS = "SERVICE_VIEW_OWNERS",
|
||||
SERVICE_VIEW_DEPENDENCIES = "SERVICE_VIEW_DEPENDENCIES",
|
||||
SERVICE_VIEW_CODE_REPOSITORIES = "SERVICE_VIEW_CODE_REPOSITORIES",
|
||||
@@ -499,6 +503,7 @@ enum PageMap {
|
||||
// Exceptions (standalone, not under Telemetry)
|
||||
EXCEPTIONS_ROOT = "EXCEPTIONS_ROOT",
|
||||
EXCEPTIONS = "EXCEPTIONS",
|
||||
EXCEPTIONS_OVERVIEW = "EXCEPTIONS_OVERVIEW",
|
||||
EXCEPTIONS_UNRESOLVED = "EXCEPTIONS_UNRESOLVED",
|
||||
EXCEPTIONS_RESOLVED = "EXCEPTIONS_RESOLVED",
|
||||
EXCEPTIONS_ARCHIVED = "EXCEPTIONS_ARCHIVED",
|
||||
|
||||
@@ -5,6 +5,65 @@ export interface ParsedStackFrame {
|
||||
}
|
||||
|
||||
export default class ProfileUtil {
|
||||
public static getProfileTypeDisplayName(profileType: string): string {
|
||||
const type: string = profileType.toLowerCase().trim();
|
||||
|
||||
switch (type) {
|
||||
case "cpu":
|
||||
return "CPU Usage";
|
||||
case "wall":
|
||||
return "Wall Clock Time";
|
||||
case "inuse_objects":
|
||||
return "Memory Objects in Use";
|
||||
case "inuse_space":
|
||||
return "Memory Space in Use";
|
||||
case "alloc_objects":
|
||||
return "Memory Allocations (Count)";
|
||||
case "alloc_space":
|
||||
return "Memory Allocations (Size)";
|
||||
case "goroutine":
|
||||
return "Goroutines";
|
||||
case "contention":
|
||||
return "Lock Contention";
|
||||
case "samples":
|
||||
return "CPU Samples";
|
||||
case "mutex":
|
||||
return "Mutex Contention";
|
||||
case "block":
|
||||
return "Blocking Operations";
|
||||
case "heap":
|
||||
return "Heap Memory";
|
||||
default:
|
||||
return profileType;
|
||||
}
|
||||
}
|
||||
|
||||
public static getProfileTypeBadgeColor(profileType: string): string {
|
||||
const type: string = profileType.toLowerCase().trim();
|
||||
|
||||
switch (type) {
|
||||
case "cpu":
|
||||
case "samples":
|
||||
return "bg-orange-100 text-orange-800";
|
||||
case "wall":
|
||||
return "bg-purple-100 text-purple-800";
|
||||
case "inuse_objects":
|
||||
case "inuse_space":
|
||||
case "alloc_objects":
|
||||
case "alloc_space":
|
||||
case "heap":
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case "goroutine":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "contention":
|
||||
case "mutex":
|
||||
case "block":
|
||||
return "bg-red-100 text-red-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
}
|
||||
|
||||
public static getFrameTypeColor(frameType: string): string {
|
||||
const type: string = frameType.toLowerCase();
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export const ServiceRoutePath: Dictionary<string> = {
|
||||
[PageMap.SERVICE_VIEW_TRACES]: `${RouteParams.ModelID}/traces`,
|
||||
[PageMap.SERVICE_VIEW_METRICS]: `${RouteParams.ModelID}/metrics`,
|
||||
[PageMap.SERVICE_VIEW_PROFILES]: `${RouteParams.ModelID}/profiles`,
|
||||
[PageMap.SERVICE_VIEW_EXCEPTIONS]: `${RouteParams.ModelID}/exceptions`,
|
||||
[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]: `${RouteParams.ModelID}/code-repositories`,
|
||||
};
|
||||
|
||||
@@ -123,6 +124,7 @@ export const LogsRoutePath: Dictionary<string> = {
|
||||
// Metrics product routes
|
||||
export const MetricsRoutePath: Dictionary<string> = {
|
||||
[PageMap.METRICS]: "",
|
||||
[PageMap.METRICS_LIST]: "list",
|
||||
[PageMap.METRIC_VIEW]: "view",
|
||||
[PageMap.METRICS_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
@@ -130,6 +132,7 @@ export const MetricsRoutePath: Dictionary<string> = {
|
||||
// Traces product routes
|
||||
export const TracesRoutePath: Dictionary<string> = {
|
||||
[PageMap.TRACES]: "",
|
||||
[PageMap.TRACES_LIST]: "list",
|
||||
[PageMap.TRACE_VIEW]: `view/${RouteParams.ModelID}`,
|
||||
[PageMap.TRACES_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
@@ -137,12 +140,14 @@ export const TracesRoutePath: Dictionary<string> = {
|
||||
// Profiles product routes
|
||||
export const ProfilesRoutePath: Dictionary<string> = {
|
||||
[PageMap.PROFILES]: "",
|
||||
[PageMap.PROFILES_LIST]: "list",
|
||||
[PageMap.PROFILE_VIEW]: `view/${RouteParams.ModelID}`,
|
||||
[PageMap.PROFILES_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
|
||||
export const ExceptionsRoutePath: Dictionary<string> = {
|
||||
[PageMap.EXCEPTIONS]: "unresolved",
|
||||
[PageMap.EXCEPTIONS]: "overview",
|
||||
[PageMap.EXCEPTIONS_OVERVIEW]: "overview",
|
||||
[PageMap.EXCEPTIONS_UNRESOLVED]: "unresolved",
|
||||
[PageMap.EXCEPTIONS_RESOLVED]: "resolved",
|
||||
[PageMap.EXCEPTIONS_ARCHIVED]: "archived",
|
||||
@@ -1483,6 +1488,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SERVICE_VIEW_EXCEPTIONS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/service/${
|
||||
ServiceRoutePath[PageMap.SERVICE_VIEW_EXCEPTIONS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/service/${
|
||||
ServiceRoutePath[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]
|
||||
@@ -2265,6 +2276,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.METRICS]: new Route(`/dashboard/${RouteParams.ProjectID}/metrics`),
|
||||
|
||||
[PageMap.METRICS_LIST]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/metrics/${
|
||||
MetricsRoutePath[PageMap.METRICS_LIST]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.METRIC_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/metrics/${
|
||||
MetricsRoutePath[PageMap.METRIC_VIEW]
|
||||
@@ -2284,6 +2301,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.TRACES]: new Route(`/dashboard/${RouteParams.ProjectID}/traces`),
|
||||
|
||||
[PageMap.TRACES_LIST]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/traces/${
|
||||
TracesRoutePath[PageMap.TRACES_LIST]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.TRACE_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/traces/${
|
||||
TracesRoutePath[PageMap.TRACE_VIEW]
|
||||
@@ -2303,6 +2326,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.PROFILES]: new Route(`/dashboard/${RouteParams.ProjectID}/profiles`),
|
||||
|
||||
[PageMap.PROFILES_LIST]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/profiles/${
|
||||
ProfilesRoutePath[PageMap.PROFILES_LIST]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.PROFILE_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/profiles/${
|
||||
ProfilesRoutePath[PageMap.PROFILE_VIEW]
|
||||
@@ -2768,6 +2797,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.EXCEPTIONS_OVERVIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/exceptions/${
|
||||
ExceptionsRoutePath[PageMap.EXCEPTIONS_OVERVIEW]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.EXCEPTIONS_UNRESOLVED]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/exceptions/${
|
||||
ExceptionsRoutePath[PageMap.EXCEPTIONS_UNRESOLVED]
|
||||
|
||||
77
App/FeatureSet/Telemetry/Index.ts
Normal file
77
App/FeatureSet/Telemetry/Index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import OTelIngestAPI from "./API/OTelIngest";
|
||||
import MetricsAPI from "./API/Metrics";
|
||||
import SyslogAPI from "./API/Syslog";
|
||||
import FluentAPI from "./API/Fluent";
|
||||
import PyroscopeAPI from "./API/Pyroscope";
|
||||
// ProbeIngest routes
|
||||
import ProbeIngestRegisterAPI from "./API/ProbeIngest/Register";
|
||||
import ProbeIngestMonitorAPI from "./API/ProbeIngest/Monitor";
|
||||
import ProbeIngestAPI from "./API/ProbeIngest/Probe";
|
||||
import IncomingEmailAPI from "./API/ProbeIngest/IncomingEmail";
|
||||
// ServerMonitorIngest routes
|
||||
import ServerMonitorAPI from "./API/ServerMonitorIngest/ServerMonitor";
|
||||
// IncomingRequestIngest routes
|
||||
import IncomingRequestAPI from "./API/IncomingRequestIngest/IncomingRequest";
|
||||
|
||||
import "./Jobs/TelemetryIngest/ProcessTelemetry";
|
||||
import { TELEMETRY_CONCURRENCY } from "./Config";
|
||||
import { startGrpcServer } from "./GrpcServer";
|
||||
|
||||
import FeatureSet from "Common/Server/Types/FeatureSet";
|
||||
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
const TELEMETRY_PREFIXES: Array<string> = ["/telemetry", "/"];
|
||||
|
||||
// Existing telemetry routes
|
||||
app.use(TELEMETRY_PREFIXES, OTelIngestAPI);
|
||||
app.use(TELEMETRY_PREFIXES, MetricsAPI);
|
||||
app.use(TELEMETRY_PREFIXES, SyslogAPI);
|
||||
app.use(TELEMETRY_PREFIXES, FluentAPI);
|
||||
app.use(TELEMETRY_PREFIXES, PyroscopeAPI);
|
||||
|
||||
/*
|
||||
* ProbeIngest routes under ["/probe-ingest", "/ingestor", "/"]
|
||||
* "/ingestor" is used for backward compatibility because probes are already deployed with this path in client environments.
|
||||
*/
|
||||
const PROBE_INGEST_PREFIXES: Array<string> = [
|
||||
"/probe-ingest",
|
||||
"/ingestor",
|
||||
"/",
|
||||
];
|
||||
app.use(PROBE_INGEST_PREFIXES, ProbeIngestRegisterAPI);
|
||||
app.use(PROBE_INGEST_PREFIXES, ProbeIngestMonitorAPI);
|
||||
app.use(PROBE_INGEST_PREFIXES, ProbeIngestAPI);
|
||||
app.use(["/probe-ingest", "/"], IncomingEmailAPI);
|
||||
|
||||
// ServerMonitorIngest routes under ["/server-monitor-ingest", "/"]
|
||||
const SERVER_MONITOR_PREFIXES: Array<string> = ["/server-monitor-ingest", "/"];
|
||||
app.use(SERVER_MONITOR_PREFIXES, ServerMonitorAPI);
|
||||
|
||||
// IncomingRequestIngest routes under ["/incoming-request-ingest", "/"]
|
||||
const INCOMING_REQUEST_PREFIXES: Array<string> = [
|
||||
"/incoming-request-ingest",
|
||||
"/",
|
||||
];
|
||||
app.use(INCOMING_REQUEST_PREFIXES, IncomingRequestAPI);
|
||||
|
||||
const TelemetryFeatureSet: FeatureSet = {
|
||||
init: async (): Promise<void> => {
|
||||
try {
|
||||
logger.info(
|
||||
`Telemetry Service - Queue concurrency: ${TELEMETRY_CONCURRENCY}`,
|
||||
);
|
||||
|
||||
// Start gRPC OTLP server on port 4317
|
||||
startGrpcServer();
|
||||
} catch (err) {
|
||||
logger.error("Telemetry FeatureSet Init Failed:");
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default TelemetryFeatureSet;
|
||||
@@ -9,24 +9,33 @@ import {
|
||||
import CaptureSpan from "Common/Server/Utils/Telemetry/CaptureSpan";
|
||||
import protobuf from "protobufjs";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import path from "path";
|
||||
|
||||
// Load proto file for OTel
|
||||
|
||||
const PROTO_DIR: string = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"ProtoFiles",
|
||||
"OTel",
|
||||
"v1",
|
||||
);
|
||||
|
||||
// Create a root namespace
|
||||
const LogsProto: protobuf.Root = protobuf.loadSync(
|
||||
"/usr/src/app/ProtoFiles/OTel/v1/logs.proto",
|
||||
path.join(PROTO_DIR, "logs.proto"),
|
||||
);
|
||||
|
||||
const TracesProto: protobuf.Root = protobuf.loadSync(
|
||||
"/usr/src/app/ProtoFiles/OTel/v1/traces.proto",
|
||||
path.join(PROTO_DIR, "traces.proto"),
|
||||
);
|
||||
|
||||
const MetricsProto: protobuf.Root = protobuf.loadSync(
|
||||
"/usr/src/app/ProtoFiles/OTel/v1/metrics.proto",
|
||||
path.join(PROTO_DIR, "metrics.proto"),
|
||||
);
|
||||
|
||||
const ProfilesProto: protobuf.Root = protobuf.loadSync(
|
||||
"/usr/src/app/ProtoFiles/OTel/v1/profiles.proto",
|
||||
path.join(PROTO_DIR, "profiles.proto"),
|
||||
);
|
||||
|
||||
// Lookup the message type
|
||||
@@ -65,7 +65,7 @@ export default class OtelLogsIngestService extends OtelIngestBaseService {
|
||||
);
|
||||
}
|
||||
|
||||
req.body = req.body.toJSON ? req.body.toJSON() : req.body;
|
||||
req.body = req.body?.toJSON ? req.body.toJSON() : req.body;
|
||||
|
||||
Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class OtelMetricsIngestService extends OtelIngestBaseService {
|
||||
);
|
||||
}
|
||||
|
||||
req.body = req.body.toJSON ? req.body.toJSON() : req.body;
|
||||
req.body = req.body?.toJSON ? req.body.toJSON() : req.body;
|
||||
|
||||
Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class OtelProfilesIngestService extends OtelIngestBaseService {
|
||||
);
|
||||
}
|
||||
|
||||
req.body = req.body.toJSON ? req.body.toJSON() : req.body;
|
||||
req.body = req.body?.toJSON ? req.body.toJSON() : req.body;
|
||||
|
||||
Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class OtelTracesIngestService extends OtelIngestBaseService {
|
||||
);
|
||||
}
|
||||
|
||||
req.body = req.body.toJSON ? req.body.toJSON() : req.body;
|
||||
req.body = req.body?.toJSON ? req.body.toJSON() : req.body;
|
||||
|
||||
Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -10,12 +10,13 @@ import BadRequestException from "Common/Types/Exception/BadRequestException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import protobuf from "protobufjs";
|
||||
import path from "path";
|
||||
import zlib from "zlib";
|
||||
import ProfilesQueueService from "./Queue/ProfilesQueueService";
|
||||
|
||||
// Load pprof proto schema
|
||||
const PprofProto: protobuf.Root = protobuf.loadSync(
|
||||
"/usr/src/app/ProtoFiles/pprof/profile.proto",
|
||||
path.resolve(__dirname, "..", "ProtoFiles", "pprof", "profile.proto"),
|
||||
);
|
||||
const PprofProfile: protobuf.Type = PprofProto.lookupType(
|
||||
"perftools.profiles.Profile",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user