Merge pull request #2383 from OneUptime/master

Release
This commit is contained in:
Simon Larsen
2026-04-02 20:58:21 +01:00
committed by GitHub
324 changed files with 2971 additions and 15468 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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: |

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -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,
),

View File

@@ -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)}%)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
},
]}

View File

@@ -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)} {" "}

View File

@@ -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 || ""}

View File

@@ -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;

View File

@@ -1728,7 +1728,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
tokenValue,
pyroscopeUrl,
)}
language="hcl"
language="nginx"
/>,
)}

View File

@@ -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;

View File

@@ -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 <></>;

View 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;

View File

@@ -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,
),

View File

@@ -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 />

View File

@@ -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;

View 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;

View File

@@ -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,
),

View File

@@ -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 />

View File

@@ -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;

View File

@@ -14,7 +14,7 @@ const ProfilesLayout: FunctionComponent<
return (
<Page
title="Profiles"
title="Performance Profiles"
breadcrumbLinks={getProfilesBreadcrumbs(path)}
sideMenu={<SideMenu />}
>

View 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;

View File

@@ -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,
),

View File

@@ -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}

View File

@@ -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 />

View File

@@ -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;

View File

@@ -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">

View File

@@ -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;

View 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;

View File

@@ -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,
),

View File

@@ -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>
);

View File

@@ -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={

View File

@@ -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={

View File

@@ -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={

View File

@@ -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={

View File

@@ -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={

View File

@@ -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];

View File

@@ -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];

View File

@@ -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];

View File

@@ -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];

View File

@@ -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",

View File

@@ -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();

View File

@@ -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]

View 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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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