diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8fd6b52a9..21215eabd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 81a9d1d359..dcf1444634 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af56db2373..1f51c0e0f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: | diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 33246fb801..8e698416f0 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -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: diff --git a/.github/workflows/test.telemetry.yaml b/.github/workflows/test.telemetry.yaml deleted file mode 100644 index 05253a0bdd..0000000000 --- a/.github/workflows/test.telemetry.yaml +++ /dev/null @@ -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 - diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 27c02a5977..113e3c796f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 \ No newline at end of file diff --git a/App/API/Metrics.ts b/App/API/Metrics.ts new file mode 100644 index 0000000000..6e778e8068 --- /dev/null +++ b/App/API/Metrics.ts @@ -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 => { + 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; diff --git a/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx new file mode 100644 index 0000000000..392af433b4 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx @@ -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(0); + const [resolvedCount, setResolvedCount] = useState(0); + const [archivedCount, setArchivedCount] = useState(0); + const [topExceptions, setTopExceptions] = useState>( + [], + ); + const [serviceSummaries, setServiceSummaries] = useState< + Array + >([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const loadDashboard: () => Promise = async (): Promise => { + 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 = servicesResult.data || []; + + // Load unresolved exception counts per service + const serviceExceptionCounts: Array = []; + + for (const service of loadedServices) { + // Get unresolved exceptions for this service + const serviceExceptions: ListResult = + 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 = + 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 ; + } + + if (error) { + return ( + { + void loadDashboard(); + }} + /> + ); + } + + const totalCount: number = unresolvedCount + resolvedCount + archivedCount; + + if (totalCount === 0) { + return ( +
+
+ + + +
+

+ No exceptions caught yet +

+

+ Once your services start reporting exceptions, you{"'"}ll see a + summary of bugs, their frequency, and which services are most + affected. +

+
+ ); + } + + return ( + + {/* Summary Stats */} +
+ +
+
+
+

Unresolved Bugs

+

+ {unresolvedCount.toLocaleString()} +

+
+
+ + + +
+
+

Needs attention

+
+
+ + +
+
+
+

Resolved

+

+ {resolvedCount.toLocaleString()} +

+
+
+ + + +
+
+

Fixed and verified

+
+
+ + +
+
+
+

Archived

+

+ {archivedCount.toLocaleString()} +

+
+
+ + + +
+
+

+ Dismissed or won{"'"}t fix +

+
+
+
+ +
+ {/* Most Frequent Exceptions */} + {topExceptions.length > 0 && ( +
+
+
+

+ Most Frequent Bugs +

+

+ Unresolved exceptions with the highest occurrence count +

+
+ + View all + +
+
+
+ {topExceptions.map( + (exception: TelemetryException, index: number) => { + const maxOccurrences: number = + topExceptions[0]?.occuranceCount || 1; + const barWidth: number = + ((exception.occuranceCount || 0) / maxOccurrences) * 100; + + return ( + +
+
+ +
+ {exception.service && ( + + {exception.service.name?.toString()} + + )} + {exception.environment && ( + + {exception.environment} + + )} +
+
+
+

+ {(exception.occuranceCount || 0).toLocaleString()} +

+

occurrences

+
+
+
+
+
+
+
+ + ); + }, + )} +
+
+
+ )} + + {/* Services Affected */} + {serviceSummaries.length > 0 && ( +
+
+

+ Affected Services +

+

+ Services with unresolved exceptions +

+
+
+
+ {serviceSummaries.map((summary: ServiceExceptionSummary) => { + return ( +
+
+ +
+
+

+ {summary.unresolvedCount} +

+

unresolved

+
+
+

+ {summary.totalOccurrences.toLocaleString()} +

+

total hits

+
+
+
+
+ ); + })} +
+
+
+ )} +
+ + ); +}; + +export default ExceptionsDashboard; diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx new file mode 100644 index 0000000000..2fa99cebfe --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx @@ -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; +} + +const MetricsDashboard: FunctionComponent = (): ReactElement => { + const [serviceSummaries, setServiceSummaries] = useState< + Array + >([]); + const [totalMetricCount, setTotalMetricCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const loadDashboard: () => Promise = async (): Promise => { + try { + setIsLoading(true); + setError(""); + + // Load services + const servicesResult: ListResult = 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 = servicesResult.data || []; + + // Load all metric types with their services + const metricsResult: ListResult = 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 = metricsResult.data || []; + setTotalMetricCount(metrics.length); + + // Build per-service summaries + const summaryMap: Map = 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 = 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 = 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 ; + } + + if (error) { + return ( + { + void loadDashboard(); + }} + /> + ); + } + + if (serviceSummaries.length === 0) { + return ( +
+
+ + + + + + + +
+

+ No metrics data yet +

+

+ Once your services start sending metrics via OpenTelemetry, you{"'"}ll + see a summary of which services are reporting, what metrics they + collect, and more. +

+
+ ); + } + + return ( + + {/* Summary Stats */} +
+
+

Total Metrics

+

+ {totalMetricCount} +

+
+
+

Services Reporting

+

+ {serviceSummaries.length} +

+
+
+

Avg Metrics per Service

+

+ {serviceSummaries.length > 0 + ? Math.round(totalMetricCount / serviceSummaries.length) + : 0} +

+
+
+ + {/* Service Cards */} +
+
+
+

+ Services Reporting Metrics +

+

+ Each service and the metrics it collects +

+
+ + View all metrics + +
+
+ {serviceSummaries.map((summary: ServiceMetricSummary) => { + return ( +
+
+ + + Active + +
+ +
+

Metrics Collected

+

+ {summary.metricCount} +

+
+ +
+

Sample Metrics

+
+ {summary.metricNames.map((name: string) => { + return ( + + {name} + + ); + })} + {summary.metricCount > summary.metricNames.length && ( + + +{summary.metricCount - summary.metricNames.length} more + + )} +
+
+ +
+ + View service metrics + +
+
+ ); + })} +
+
+
+ ); +}; + +export default MetricsDashboard; diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx index e68cdff394..09e414561f 100644 --- a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx @@ -43,9 +43,9 @@ const MetricsTable: FunctionComponent = ( 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( diff --git a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx index 710e12b922..9d3b6dcc98 100644 --- a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx +++ b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx @@ -108,7 +108,7 @@ const DashboardNavbar: FunctionComponent = ( }, { 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 = ( }, { 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 = ( 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 = ( }, { title: "Exceptions", - description: "Catch and fix bugs early.", + description: "Track and resolve bugs across your services.", route: RouteUtil.populateRouteParams( RouteMap[PageMap.EXCEPTIONS] as Route, ), diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/DiffFlamegraph.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/DiffFlamegraph.tsx index b3082ba0db..bdfd6d17d1 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/DiffFlamegraph.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/DiffFlamegraph.tsx @@ -184,7 +184,8 @@ const DiffFlamegraph: FunctionComponent = ( ) { return (
- No profile data found in the selected time ranges. + No performance data found in the selected time ranges. Try adjusting the + time periods.
); } @@ -323,14 +324,14 @@ const DiffFlamegraph: FunctionComponent = ( )}
- Legend: + What the colors mean: - Regression (slower) + Got slower - Improvement (faster) + Got faster @@ -364,9 +365,9 @@ const DiffFlamegraph: FunctionComponent = (
{tooltip.fileName}
)}
- Baseline: {tooltip.baselineValue.toLocaleString()} + Before: {tooltip.baselineValue.toLocaleString()}
-
Comparison: {tooltip.comparisonValue.toLocaleString()}
+
After: {tooltip.comparisonValue.toLocaleString()}
0 @@ -376,7 +377,7 @@ const DiffFlamegraph: FunctionComponent = ( : "" } > - Delta: {tooltip.delta > 0 ? "+" : ""} + Change: {tooltip.delta > 0 ? "+" : ""} {tooltip.delta.toLocaleString()} ( {tooltip.deltaPercent >= 0 ? "+" : ""} {tooltip.deltaPercent.toFixed(1)}%) diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFlamegraph.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFlamegraph.tsx index 111e9c0f0e..f8553cb40e 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFlamegraph.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFlamegraph.tsx @@ -211,7 +211,8 @@ const ProfileFlamegraph: FunctionComponent = ( if (samples.length === 0) { return (
- 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.
); } @@ -325,19 +326,25 @@ const ProfileFlamegraph: FunctionComponent = ( )}
- Frame Types: - {["kernel", "native", "jvm", "cpython", "go", "v8js", "unknown"].map( - (type: string) => { - return ( - - - {type} - - ); - }, - )} + Code Type: + {[ + { 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 ( + + + {item.label} + + ); + })}
= ( {tooltip.fileName && (
{tooltip.fileName}
)} -
Self: {tooltip.selfValue.toLocaleString()}
-
Total: {tooltip.totalValue.toLocaleString()}
+
+ Own Time: {tooltip.selfValue.toLocaleString()} +
+
Total Time: {tooltip.totalValue.toLocaleString()}
)}
diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFunctionList.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFunctionList.tsx index 2036acec3a..7027bee27c 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFunctionList.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileFunctionList.tsx @@ -204,7 +204,7 @@ const ProfileFunctionList: FunctionComponent = ( if (samples.length === 0) { return (
- No profile samples found for this profile. + No performance data found for this profile.
); } @@ -228,7 +228,7 @@ const ProfileFunctionList: FunctionComponent = ( handleSort("fileName"); }} > - File{getSortIndicator("fileName")} + Source File{getSortIndicator("fileName")} = ( handleSort("selfValue"); }} > - Self Value{getSortIndicator("selfValue")} + Own Time{getSortIndicator("selfValue")} = ( handleSort("totalValue"); }} > - Total Value{getSortIndicator("totalValue")} + Total Time{getSortIndicator("totalValue")} = ( handleSort("sampleCount"); }} > - Samples{getSortIndicator("sampleCount")} + Occurrences{getSortIndicator("sampleCount")} diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTable.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTable.tsx index d19cca85bb..568701a67a 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTable.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTable.tsx @@ -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 = ( 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 = ( profileType: true, }, type: FieldType.Text, - title: "Profile Type", + title: "Type", }, { field: { @@ -259,7 +265,7 @@ const ProfileTable: FunctionComponent = ( startTime: true, }, type: FieldType.DateTime, - title: "Start Time", + title: "Captured At", }, { field: { @@ -273,20 +279,6 @@ const ProfileTable: FunctionComponent = ( ]} 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 = ( ); }, }, + { + 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 ( + + {displayName} + + ); + }, + }, { field: { sampleCount: true, }, - title: "Samples", + title: "Data Points", type: FieldType.Number, }, { field: { startTime: true, }, - title: "Start Time", + title: "Captured At", type: FieldType.DateTime, }, ]} diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTimeline.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTimeline.tsx index ec0d529d0a..2db16f875b 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTimeline.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTimeline.tsx @@ -168,7 +168,7 @@ const ProfileTimeline: FunctionComponent = (
- Profile Density ({profiles.length} profiles) + Activity ({profiles.length} profiles captured) {OneUptimeDate.getDateAsLocalFormattedString(props.startTime, true)} —{" "} diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTypeSelector.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTypeSelector.tsx index 4da220bc71..a0349da705 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTypeSelector.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfileTypeSelector.tsx @@ -12,12 +12,12 @@ interface ProfileTypeOption { const profileTypeOptions: Array = [ { 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 = ( @@ -25,7 +25,7 @@ const ProfileTypeSelector: FunctionComponent = ( ): ReactElement => { return (
- +