Compare commits

...

107 Commits

Author SHA1 Message Date
Nawaz Dhandala
dd37b8a05e fix: update component search tests to handle split titles due to highlighting 2026-04-05 20:07:58 +01:00
Nawaz Dhandala
23f5ffc840 fix: add waitUntil option for navigation in CreateMonitor and CreateProject tests 2026-04-05 20:00:03 +01:00
Nawaz Dhandala
875dbccad3 fix: update search placeholder text for clarity in ComponentsModal tests 2026-04-05 19:57:10 +01:00
Nawaz Dhandala
fb8fa899b0 fix: update comments for clarity on telemetry route mounting during feature-set initialization 2026-04-03 20:46:05 +01:00
Nawaz Dhandala
4bad603db2 fix: update Dockerfile to use Start.dev.sh for development environment 2026-04-03 20:24:41 +01:00
Nawaz Dhandala
720399c8b8 fix: reorganize telemetry route mounting for better clarity and maintainability 2026-04-03 20:14:11 +01:00
Nawaz Dhandala
37e4f28e57 fix: update project dashboard URL regex to include optional home path 2026-04-03 16:33:55 +01:00
Nawaz Dhandala
0502eb5ebe fix: enforce resource request requirements for CPU and memory utilization thresholds in app configuration 2026-04-03 14:37:12 +01:00
Nawaz Dhandala
191569eb3d fix: add Locator type for modal submit button and plan option in project and monitor creation tests 2026-04-03 14:33:17 +01:00
Nawaz Dhandala
2770f9a515 fix: standardize project dashboard URL regex and improve modal button handling in project and monitor creation tests 2026-04-03 14:30:22 +01:00
Nawaz Dhandala
788eeae500 fix: remove duplicate project selection callback in DashboardProjectPicker 2026-04-03 14:23:04 +01:00
Nawaz Dhandala
a8497c497c Push changes 2026-04-03 12:06:42 +01:00
Nawaz Dhandala
b7a4214fa4 fix: add missing declaration for CSS module in TypeScript definitions 2026-04-03 12:01:11 +01:00
Nawaz Dhandala
5e9034dd76 refactor: simplify arrow function syntax and improve readability in dashboard components 2026-04-03 11:43:37 +01:00
Nawaz Dhandala
26bcc69fa2 Enhance Traces Dashboard with latency percentiles and service health metrics
- Added global P50, P95, and P99 latency calculations to the TracesDashboard component.
- Introduced a new formatDuration function to format latency values for display.
- Updated service summaries to include per-service P50 and P95 latencies.
- Improved the layout of the dashboard to display overall requests, error rates, and latency metrics.
- Refactored service health indicators to visually represent error rates with color coding.
- Adjusted the display of recent errors and slow requests for better user experience.
- Updated E2E tests for admin and public dashboards to include response type checks.
2026-04-03 11:34:06 +01:00
Nawaz Dhandala
577d8d2fba feat: improve search functionality in ComponentsModal with scoring and highlighting 2026-04-03 09:59:54 +01:00
Nawaz Dhandala
2b9aaa9929 feat: enhance search functionality in ComponentsModal with improved UI and state management 2026-04-03 09:49:30 +01:00
Nawaz Dhandala
cf166da6de Merge branch 'master' into release 2026-04-03 09:38:46 +01:00
Nawaz Dhandala
92a48f1e17 feat: add status check tests for Admin and Public dashboards, remove obsolete Telemetry status checks 2026-04-03 09:37:35 +01:00
Nawaz Dhandala
f0d0d81a9b fix: update status-check script and README for endpoint consistency 2026-04-03 09:35:33 +01:00
Nawaz Dhandala
a2dc9bf1c8 Merge branch 'master' of https://github.com/OneUptime/oneuptime into codex/fix-ci-10-0-45 2026-04-03 09:22:07 +01:00
Nawaz Dhandala
263d745d0a fix: update GoReleaser action to v6.1.0 and clean up MetricsDashboard component 2026-04-03 09:22:00 +01:00
Simon Larsen
d108cd484e Merge pull request #2384 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-04-03 09:09:57 +01:00
simlarsen
148813786a chore: npm audit fix 2026-04-03 02:38:34 +00:00
Nawaz Dhandala
8101f4a459 feat: add parseBody middleware for handling gzip and protobuf requests in OpenTelemetry routes 2026-04-02 23:46:57 +01:00
Nawaz Dhandala
46a698b4be fix: update nodemon and start scripts to include --no-node-snapshot option; add prepare-native-deps script for isolated-vm management 2026-04-02 23:24:12 +01:00
Nawaz Dhandala
8d07271aa1 chore: update package dependencies and enhance frontend script for missing installations 2026-04-02 23:16:06 +01:00
Nawaz Dhandala
f5ef80e544 refactor: remove unnecessary blank lines in app initialization 2026-04-02 22:00:54 +01:00
Nawaz Dhandala
292a37397d refactor: streamline database connection logic in app initialization 2026-04-02 21:50:12 +01:00
Nawaz Dhandala
abb3942c44 refactor: clean up code formatting and remove unused imports in Metrics components 2026-04-02 21:38:14 +01:00
Nawaz Dhandala
10d09ac4af chore: update version to 10.0.45 2026-04-02 21:19:49 +01:00
Nawaz Dhandala
64c31e9e7a Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2026-04-02 21:00:09 +01:00
Nawaz Dhandala
d64194c18e fix: correct local time formatting in OneUptimeDate class 2026-04-02 20:59:45 +01:00
Simon Larsen
2d13a52287 Merge pull request #2383 from OneUptime/master
Release
2026-04-02 20:58:21 +01:00
Nawaz Dhandala
a54234609f feat: implement MetricsDashboard and MetricsListPage components, update routing and descriptions for improved clarity 2026-04-02 19:10:10 +01:00
Nawaz Dhandala
214c9e013c feat: enhance empty state visualization in TracesDashboard with improved SVG graphics 2026-04-02 18:35:11 +01:00
Nawaz Dhandala
b0c9de4d82 feat: add Exceptions page and integrate with routing and side menu 2026-04-02 18:20:36 +01:00
Nawaz Dhandala
e98b424168 refactor: improve code readability by formatting and using optional chaining for req.body 2026-04-02 18:11:42 +01:00
Nawaz Dhandala
7521fe218d feat: remove unused OneUptimeDate import from ExceptionsDashboard component 2026-04-02 17:53:42 +01:00
Nawaz Dhandala
1f3d85d7a1 feat: add PublicDashboard volume to docker-compose for development 2026-04-02 17:39:35 +01:00
Nawaz Dhandala
058c52f79d feat: update protobufjs version and add gRPC dependencies 2026-04-02 17:35:08 +01:00
Nawaz Dhandala
8af6e48d70 feat: add ExceptionsOverview page and dashboard components, update routing and breadcrumbs for exceptions 2026-04-02 17:28:01 +01:00
Nawaz Dhandala
7569a50c56 feat: add TracesDashboard and TracesListPage components, update routing and breadcrumbs for traces 2026-04-02 17:24:19 +01:00
Nawaz Dhandala
20f314512d feat: add ProfilesDashboard and ProfilesListPage components, update routing and breadcrumbs for profiles 2026-04-02 17:15:42 +01:00
Nawaz Dhandala
cdbbcdfe27 refactor: update proto file paths to use relative directory resolution 2026-04-02 15:23:13 +01:00
Nawaz Dhandala
4e2ca87752 refactor: improve readability of messages and code formatting in various components 2026-04-02 14:33:46 +01:00
Nawaz Dhandala
54a79a8100 feat: implement combined queue size metrics for KEDA autoscaling 2026-04-02 14:29:39 +01:00
Nawaz Dhandala
eb4010dfa5 feat: add CPU and memory utilization metrics for KEDA autoscaling 2026-04-02 14:23:39 +01:00
Nawaz Dhandala
407d4e3687 feat: add KEDA autoscaling configuration for worker and telemetry queue metrics 2026-04-02 14:20:57 +01:00
Nawaz Dhandala
6f7907102b refactor: remove telemetry hostname references and update backend proxy settings 2026-04-02 14:09:26 +01:00
Nawaz Dhandala
5f398bdb31 Add utility classes for telemetry: Monitor, StackTrace, and Syslog parsing
- Implemented MonitorUtil for managing monitor secrets and populating them in monitor steps and tests.
- Created StackTraceParser to parse and structure stack traces from various programming languages.
- Developed SyslogParser to handle and parse syslog messages in both RFC 5424 and RFC 3164 formats.
2026-04-02 14:04:13 +01:00
Nawaz Dhandala
69c6b332c1 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-04-02 12:37:07 +01:00
Nawaz Dhandala
e15a934b3f refactor: update terminology and improve messaging for performance profiling components 2026-04-02 12:37:03 +01:00
Simon Larsen
3a62729c03 Merge pull request #2381 from OneUptime/merge-workflow
Merge workflow
2026-04-02 12:31:31 +01:00
Nawaz Dhandala
23da31b50c fix: update iOS code signing parameters in release workflow for improved build stability 2026-04-02 12:24:30 +01:00
Nawaz Dhandala
4e33cd7c1b fix: ensure page stability after project creation by waiting for network to be idle 2026-04-02 09:02:44 +01:00
Simon Larsen
d97f17b1cf Merge pull request #2382 from OneUptime/master
Release
2026-04-01 22:22:40 +01:00
Nawaz Dhandala
4bdf9943e4 feat: add data-testid attribute to radio button labels for improved testing 2026-04-01 22:21:43 +01:00
Nawaz Dhandala
a4c5be8665 feat: Integrate GoReleaser for Terraform provider build and publish process; enhance publish script for better asset management 2026-04-01 22:10:13 +01:00
Nawaz Dhandala
ea71c8bd75 feat: Implement Workflow API and Queue Management
- Added ManualAPI for manually triggering workflows via GET and POST requests.
- Introduced WorkflowAPI for updating workflows with authorization checks.
- Created documentation for JavaScript and Webhook components.
- Established WorkflowFeatureSet to initialize routing and job processing.
- Developed QueueWorkflow service for managing workflow queue operations.
- Implemented RunWorkflow service to execute workflows with error handling and logging.
- Added utility for loading component metadata dynamically.
2026-04-01 22:05:19 +01:00
Nawaz Dhandala
043707d0cb fix: update blog repo clone command to use --depth 1 for faster cloning 2026-04-01 19:52:32 +01:00
Nawaz Dhandala
991916b2de chore: bump version to 10.0.44 2026-04-01 18:54:28 +01:00
Nawaz Dhandala
5d3885c8a5 feat: enhance MonitorCustomMetrics to include ListResult type for API response; improve query documentation 2026-04-01 18:54:11 +01:00
Nawaz Dhandala
da44cd34f8 feat: update MonitorCustomMetrics to fetch custom metrics from AnalyticsModelAPI; enhance metric name extraction logic 2026-04-01 18:49:57 +01:00
Nawaz Dhandala
ffa2d3f008 refactor: clean up code formatting and improve readability across multiple components 2026-04-01 18:18:20 +01:00
Nawaz Dhandala
d8aea2627b feat: add MonitorCustomMetrics component for displaying custom metrics; enhance MonitorMetrics to include custom metrics tab 2026-04-01 15:35:11 +01:00
Nawaz Dhandala
9756f5a117 feat: update ChartGroup button styles for improved accessibility and visual consistency 2026-04-01 15:11:47 +01:00
Nawaz Dhandala
c8cd97437e feat: update UptimeBarTooltip styles with adjusted font sizes and weights for improved readability 2026-04-01 15:08:43 +01:00
Nawaz Dhandala
249241dfd4 feat: enhance UptimeBarTooltip with improved layout, color adjustments, and incident display; optimize status breakdown and tooltip styles 2026-04-01 15:07:20 +01:00
Nawaz Dhandala
16e2c2cb39 feat: enhance UptimeBarTooltip with improved color handling, layout adjustments, and incident display; optimize status breakdown and tooltip styles 2026-04-01 15:03:24 +01:00
Nawaz Dhandala
ecbca3208f feat: add onIncidentClick handler to various components for incident navigation; enhance Tooltip with animation support 2026-04-01 14:58:58 +01:00
Nawaz Dhandala
505c143ddf feat: enhance MetricCharts and ChartGroup with metric info handling and modal display; update UptimeBarTooltip styles and Tooltip theme 2026-04-01 14:45:21 +01:00
Nawaz Dhandala
c4aab31056 feat: enhance DayUptimeGraph and UptimeBarTooltip with status duration handling and improved tooltip display 2026-04-01 14:40:34 +01:00
Nawaz Dhandala
cdb63031d8 feat: add custom metrics capturing functionality in custom code and synthetic monitors 2026-04-01 14:34:05 +01:00
Nawaz Dhandala
464455eff3 feat: implement captured metrics handling in custom code and synthetic monitors 2026-04-01 14:30:01 +01:00
Nawaz Dhandala
c7cfd7aa67 feat: update isolated-vm dependency to version 6.1.2 2026-04-01 14:23:31 +01:00
Nawaz Dhandala
832b87e6d5 feat: implement incident handling in uptime graphs with tooltips and modals for better user experience 2026-04-01 12:42:00 +01:00
Nawaz Dhandala
678e9614bf feat: add NoDataMessage component and integrate it into AreaChart, BarChart, and LineChart for improved no data handling 2026-04-01 12:14:27 +01:00
Nawaz Dhandala
ac6c53ad85 feat: refactor AreaChart, BarChart, and LineChart components to handle no data state more effectively 2026-04-01 12:12:25 +01:00
Nawaz Dhandala
22bf4de6fd feat: add no data available message to AreaChart, BarChart, and LineChart components 2026-04-01 12:06:42 +01:00
Nawaz Dhandala
dacf71a75d feat: add chartType property to MetricQueryConfigData for MonitorAlertMetrics and MonitorIncidentMetrics 2026-04-01 11:44:58 +01:00
Nawaz Dhandala
213c755f97 feat: add support for DateTime64 type in value parsing for improved data handling 2026-04-01 11:27:04 +01:00
Nawaz Dhandala
ac39602ef6 feat: enhance Metrics and Incident services with Search integration for improved data handling 2026-04-01 09:31:05 +01:00
Nawaz Dhandala
848fd2c30b fix: correct order of sentences in AGENTS.md for clarity 2026-04-01 09:09:26 +01:00
Nawaz Dhandala
63dd84339e feat: update Pyroscope endpoints and documentation for improved profiling integration 2026-03-31 22:50:13 +01:00
Nawaz Dhandala
e3ca08c69f Implement feature X to enhance user experience and optimize performance 2026-03-31 20:33:28 +01:00
Nawaz Dhandala
3276ab3641 refactor: remove DisableTelemetry check from Profiling initialization 2026-03-31 20:18:35 +01:00
Nawaz Dhandala
675cfa4682 feat: add enableProfiling flag to multiple components for enhanced profiling support 2026-03-31 20:00:33 +01:00
Nawaz Dhandala
f28306ce68 refactor: remove unused telemetry disable flags from configuration 2026-03-31 15:58:43 +01:00
Nawaz Dhandala
9b9ac62c77 feat: add id prop to BasicForm container for improved accessibility 2026-03-31 14:25:53 +01:00
Nawaz Dhandala
574cac7d64 refactor: clean up code formatting and improve readability across multiple files 2026-03-31 14:06:05 +01:00
Nawaz Dhandala
414f7cebc7 feat: update telemetry documentation and replace OTLP URLs with OneUptime base URL 2026-03-31 14:02:50 +01:00
Nawaz Dhandala
e30f2587e8 feat: integrate Pyroscope for performance profiling
- Added @pyroscope/nodejs dependency to package.json and package-lock.json.
- Implemented Pyroscope API routes in Telemetry/Index.ts.
- Created new Pyroscope API handler in Telemetry/API/Pyroscope.ts to manage profile ingestion.
- Defined pprof protocol buffer schema in Telemetry/ProtoFiles/pprof/profile.proto.
- Developed PyroscopeIngestService for processing and converting pprof profiles to OTLP format.
- Enhanced error handling and request validation in the Pyroscope ingestion process.
2026-03-31 13:55:14 +01:00
Nawaz Dhandala
d7a339b9aa feat: Add profiling support across services and implement new metrics
- Integrated profiling initialization in Probe, Telemetry, TestServer, and Worker services.
- Added environment variables for enabling profiling in various services.
- Created Profiling utility to handle CPU profiling and send data to OTLP endpoint.
- Introduced new metric types for exceptions, spans, and dashboards.
- Developed utility classes for handling alert and incident metrics.
- Added new React components for displaying alert and incident metrics in the dashboard.
2026-03-31 13:44:59 +01:00
Nawaz Dhandala
fe5329a1aa feat: add new dashboard templates for Metrics, Trace, Exception, and Profiles 2026-03-31 12:38:13 +01:00
Nawaz Dhandala
043ddebc6c feat: add webhook secret key functionality to workflows and update related components 2026-03-31 12:22:17 +01:00
Nawaz Dhandala
67b9d245ec feat: enhance resource display with dynamic table headers in Kubernetes analysis 2026-03-31 12:07:55 +01:00
Nawaz Dhandala
856e1f4715 chore: update version to 10.0.43 2026-03-31 11:55:06 +01:00
Nawaz Dhandala
72da710326 feat: add provisioning profile UUID to environment for iOS build 2026-03-31 11:44:47 +01:00
Nawaz Dhandala
9fc6871a1f feat: add user authorization middleware to workflow run endpoints 2026-03-31 11:39:31 +01:00
Nawaz Dhandala
7add10642f feat: add E2E tests for monitor and project creation workflows
chore: update ClickHouse config to disable no_password authentication
2026-03-31 11:20:58 +01:00
Nawaz Dhandala
34b6c198cb chore: update version to 10.0.42 2026-03-31 08:16:08 +01:00
Nawaz Dhandala
3dda45d2cc feat: add profile data ingestion to metered plan reporting 2026-03-31 08:15:28 +01:00
Nawaz Dhandala
2fd7ede52f feat: add validation for SAML Assertion length in response handling 2026-03-30 21:52:52 +01:00
Nawaz Dhandala
599e8dda1d feat: add system log retention configuration for ClickHouse with TTL settings 2026-03-30 21:27:40 +01:00
Nawaz Dhandala
21062dab44 feat: enhance cleanup CronJob configuration with concurrency and history limits 2026-03-30 19:23:31 +01:00
Nawaz Dhandala
3477593e11 feat: add ServiceAccount for cleanup CronJob 2026-03-30 19:18:22 +01:00
436 changed files with 12506 additions and 17964 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
@@ -1035,7 +869,7 @@ jobs:
cache: true
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6.1.0
with:
install-only: true
@@ -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:
@@ -1636,6 +1466,8 @@ jobs:
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/"$PROFILE_UUID".mobileprovision
echo "PROFILE_UUID=$PROFILE_UUID" >> $GITHUB_ENV
- name: Build archive
env:
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
@@ -1654,6 +1486,7 @@ jobs:
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="iPhone Distribution" \
DEVELOPMENT_TEAM="$IOS_TEAM_ID" \
PROVISIONING_PROFILE="${{ env.PROFILE_UUID }}" \
MARKETING_VERSION=${{ needs.read-version.outputs.major_minor }} \
CURRENT_PROJECT_VERSION=${{ needs.generate-build-number.outputs.build_number }}
@@ -1872,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

View File

@@ -1 +1 @@
This is a local development server hosted at HOST env variable (please read config.env file). When you make any changes to the codebase the server hot-reloads. Please make sure you wait for it to restart to test. This project is hosted on docker compose for local development. If you need access to the database during development, credentials are in config.env file.
This is a local development server hosted at HOST env variable (please read config.env file). This project is hosted on docker compose for local development. When you make any changes to the codebase the container hot-reloads. Please make sure you wait for it to restart to test. If you need access to the database during development, credentials are in config.env file.

View File

@@ -11,6 +11,7 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import Profiling from "Common/Server/Utils/Profiling";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import "ejs";
@@ -23,6 +24,11 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
serviceName: APP_NAME,
});
// Initialize profiling (opt-in via ENABLE_PROFILING env var)
Profiling.init({
serviceName: APP_NAME,
});
logger.info("AI Agent Service - Starting...");
// init the app

View File

@@ -48,6 +48,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -80,7 +81,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -1488,9 +1489,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -2225,9 +2226,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3753,9 +3754,9 @@
}
},
"node_modules/nodemon/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4440,9 +4441,9 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {

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

@@ -52,6 +52,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -84,7 +85,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -802,9 +803,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -930,9 +931,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"

View File

@@ -51,6 +51,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -83,7 +84,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -786,9 +787,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -914,9 +915,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"

View File

@@ -55,6 +55,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -87,7 +88,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -1083,9 +1084,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1380,9 +1381,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"

View File

@@ -0,0 +1,734 @@
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 OneUptimeDate from "Common/Types/Date";
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 [recentExceptions, setRecentExceptions] = 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()!;
const [
unresolvedResult,
resolvedResult,
archivedResult,
topExceptionsResult,
recentExceptionsResult,
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: 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: 8,
skip: 0,
sort: {
lastSeenAt: 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 || []);
setRecentExceptions(recentExceptionsResult.data || []);
const loadedServices: Array<Service> = servicesResult.data || [];
// Load unresolved exception counts per service
const serviceExceptionCounts: Array<ServiceExceptionSummary> = [];
for (const service of loadedServices) {
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,
});
}
}
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-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-green-50 flex items-center justify-center mb-5">
<svg
className="h-8 w-8 text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No exceptions caught yet
</h3>
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start reporting exceptions, you{"'"}ll see bug
frequency, affected services, and resolution status here.
</p>
</div>
);
}
const resolutionRate: number =
totalCount > 0
? Math.round(((resolvedCount + archivedCount) / totalCount) * 100)
: 0;
// Count how many of the top exceptions were first seen in last 24h
const now: Date = OneUptimeDate.getCurrentDate();
const oneDayAgo: Date = OneUptimeDate.addRemoveHours(now, -24);
const newTodayCount: number = topExceptions.filter(
(e: TelemetryException) => {
return e.firstSeenAt && new Date(e.firstSeenAt) > oneDayAgo;
},
).length;
const maxServiceBugs: number =
serviceSummaries.length > 0 ? serviceSummaries[0]!.unresolvedCount : 1;
return (
<Fragment>
{/* Unresolved Alert Banner */}
{unresolvedCount > 0 && (
<AppLink
className="block mb-6"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
)}
>
<div
className={`rounded-xl p-4 flex items-center justify-between ${unresolvedCount > 20 ? "bg-red-50 border border-red-200" : unresolvedCount > 5 ? "bg-amber-50 border border-amber-200" : "bg-blue-50 border border-blue-200"}`}
>
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center ${unresolvedCount > 20 ? "bg-red-100" : unresolvedCount > 5 ? "bg-amber-100" : "bg-blue-100"}`}
>
<svg
className={`h-5 w-5 ${unresolvedCount > 20 ? "text-red-600" : unresolvedCount > 5 ? "text-amber-600" : "text-blue-600"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
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>
<div>
<p
className={`text-sm font-semibold ${unresolvedCount > 20 ? "text-red-800" : unresolvedCount > 5 ? "text-amber-800" : "text-blue-800"}`}
>
{unresolvedCount} unresolved{" "}
{unresolvedCount === 1 ? "bug" : "bugs"} need attention
</p>
<p
className={`text-xs mt-0.5 ${unresolvedCount > 20 ? "text-red-600" : unresolvedCount > 5 ? "text-amber-600" : "text-blue-600"}`}
>
{newTodayCount > 0
? `${newTodayCount} new in the last 24 hours`
: "Click to view and triage"}
</p>
</div>
</div>
<svg
className={`h-5 w-5 ${unresolvedCount > 20 ? "text-red-400" : unresolvedCount > 5 ? "text-amber-400" : "text-blue-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</div>
</AppLink>
)}
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<AppLink
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
)}
>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-red-200 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Unresolved</p>
<div className="h-8 w-8 rounded-lg bg-red-50 flex items-center justify-center">
<svg
className="h-4 w-4 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-red-600 mt-2">
{unresolvedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">needs attention</p>
</div>
</AppLink>
<AppLink
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_RESOLVED] as Route,
)}
>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-green-200 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Resolved</p>
<div className="h-8 w-8 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-4 w-4 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-green-600 mt-2">
{resolvedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">fixed</p>
</div>
</AppLink>
<AppLink
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_ARCHIVED] as Route,
)}
>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-gray-300 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Archived</p>
<div className="h-8 w-8 rounded-lg bg-gray-100 flex items-center justify-center">
<svg
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-600 mt-2">
{archivedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">dismissed</p>
</div>
</AppLink>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Resolution Rate</p>
<div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4 w-4 text-indigo-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{resolutionRate}%
</p>
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden mt-2">
<div
className="h-full rounded-full bg-indigo-400"
style={{ width: `${resolutionRate}%` }}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Most Frequent Exceptions - takes 2 columns */}
{topExceptions.length > 0 && (
<div className="lg:col-span-2">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500" />
<h3 className="text-base font-semibold text-gray-900">
Most Frequent Bugs
</h3>
</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-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{topExceptions.map(
(exception: TelemetryException, index: number) => {
const maxOccurrences: number =
topExceptions[0]?.occuranceCount || 1;
const barWidth: number =
((exception.occuranceCount || 0) / maxOccurrences) * 100;
const isNewToday: boolean = Boolean(
exception.firstSeenAt &&
new Date(exception.firstSeenAt) > oneDayAgo,
);
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.5">
<div className="min-w-0 flex-1 mr-3">
<div className="flex items-center gap-2 mb-1">
<TelemetryExceptionElement
message={
exception.message ||
exception.exceptionType ||
"Unknown exception"
}
isResolved={exception.isResolved || false}
isArchived={exception.isArchived || false}
className="text-sm"
/>
{isNewToday && (
<span className="flex-shrink-0 text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded font-medium">
New
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1">
{exception.service && (
<span className="text-xs text-gray-500">
{exception.service.name?.toString()}
</span>
)}
{exception.exceptionType && (
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded font-mono">
{exception.exceptionType}
</span>
)}
{exception.environment && (
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
{exception.environment}
</span>
)}
{exception.lastSeenAt && (
<span className="text-xs text-gray-400">
{OneUptimeDate.fromNow(
new Date(exception.lastSeenAt),
)}
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<p className="text-sm font-bold text-gray-900">
{(exception.occuranceCount || 0).toLocaleString()}
</p>
<p className="text-xs text-gray-400">hits</p>
</div>
</div>
<div className="mt-1.5">
<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>
)}
{/* Right sidebar: Affected Services + Recently Seen */}
<div className="space-y-6">
{/* Affected Services */}
{serviceSummaries.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-amber-500" />
<h3 className="text-base font-semibold text-gray-900">
Affected Services
</h3>
</div>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{serviceSummaries.map((summary: ServiceExceptionSummary) => {
const barWidth: number =
(summary.unresolvedCount / maxServiceBugs) * 100;
return (
<div
key={summary.service.id?.toString()}
className="px-4 py-3"
>
<div className="flex items-center justify-between mb-2">
<TelemetryServiceElement
telemetryService={summary.service}
/>
<div className="flex items-center gap-3">
<div className="text-right">
<span className="text-sm font-bold text-red-600">
{summary.unresolvedCount}
</span>
<span className="text-xs text-gray-400 ml-1">
bugs
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-red-400"
style={{
width: `${Math.max(barWidth, 3)}%`,
}}
/>
</div>
<span className="text-xs text-gray-400 flex-shrink-0">
{summary.totalOccurrences.toLocaleString()} hits
</span>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Recently Active */}
{recentExceptions.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<h3 className="text-base font-semibold text-gray-900">
Recently Active
</h3>
</div>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{recentExceptions
.slice(0, 5)
.map((exception: TelemetryException, index: number) => {
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,
)
}
>
<p className="text-sm text-gray-900 truncate font-medium">
{exception.message ||
exception.exceptionType ||
"Unknown"}
</p>
<div className="flex items-center gap-2 mt-1">
{exception.service && (
<span className="text-xs text-gray-500">
{exception.service.name?.toString()}
</span>
)}
{exception.lastSeenAt && (
<span className="text-xs text-gray-400">
{OneUptimeDate.fromNow(
new Date(exception.lastSeenAt),
)}
</span>
)}
</div>
</AppLink>
);
})}
</div>
</div>
</div>
)}
</div>
</div>
</Fragment>
);
};
export default ExceptionsDashboard;

View File

@@ -257,7 +257,14 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
if (props.monitorType === MonitorType.CustomJavaScriptCode) {
codeEditorPlaceholder = `
// You can use axios, http modules here.
await axios.get('https://example.com');
const response = await axios.get('https://example.com');
// To capture custom metrics, use oneuptime.captureMetric(name, value, attributes)
// These metrics can be charted on dashboards via the Metric Explorer.
oneuptime.captureMetric('api.response.time', response.data.latency);
oneuptime.captureMetric('api.queue.depth', response.data.queueDepth, {
region: 'us-east-1'
});
// when you want to return a value, use return statement with data as a prop.
@@ -275,6 +282,7 @@ return {
// - page: Playwright Page object to interact with the browser
// - browserType: Browser type in the current run context - Chromium, Firefox, Webkit
// - screenSizeType: Screen size type in the current run context - Mobile, Tablet, Desktop
// - oneuptime.captureMetric: Capture custom metrics for dashboards
await page.goto('https://playwright.dev/');
@@ -286,6 +294,11 @@ const screenshots = {};
screenshots['screenshot-name'] = await page.screenshot(); // you can save multiple screenshots and have them with different names.
// To capture custom metrics, use oneuptime.captureMetric(name, value, attributes)
// These metrics can be charted on dashboards via the Metric Explorer.
const startTime = Date.now();
await page.waitForSelector('h1');
oneuptime.captureMetric('page.load.time', Date.now() - startTime);
// To log data, use console.log
console.log('Hello World');

View File

@@ -284,9 +284,6 @@ const DashboardProjectPicker: FunctionComponent<ComponentProps> = (
if (project && props.onProjectSelected) {
props.onProjectSelected(project);
}
if (project && props.onProjectSelected) {
props.onProjectSelected(project);
}
setShowModal(false);
props.onProjectModalClose();
}}

View File

@@ -3,8 +3,10 @@ import OneUptimeDate from "Common/Types/Date";
import XAxisType from "Common/UI/Components/Charts/Types/XAxis/XAxisType";
import ChartGroup, {
Chart,
ChartMetricInfo,
ChartType,
} from "Common/UI/Components/Charts/ChartGroup/ChartGroup";
import Dictionary from "Common/Types/Dictionary";
import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult";
import { XAxisAggregateType } from "Common/UI/Components/Charts/Types/XAxis/XAxis";
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
@@ -201,6 +203,35 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
});
}
// Build metric info for the info icon modal
const metricAttributes: Dictionary<string> = {};
const filterAttributes:
| Dictionary<string | boolean | number>
| undefined = queryConfig.metricQueryData.filterData.attributes as
| Dictionary<string | boolean | number>
| undefined;
if (filterAttributes) {
for (const key of Object.keys(filterAttributes)) {
metricAttributes[key] = String(filterAttributes[key]);
}
}
const metricInfo: ChartMetricInfo = {
metricName:
queryConfig.metricQueryData.filterData.metricName?.toString() || "",
aggregationType:
queryConfig.metricQueryData.filterData.aggegationType?.toString() ||
"",
attributes:
Object.keys(metricAttributes).length > 0
? metricAttributes
: undefined,
groupByAttribute:
queryConfig.metricQueryData.filterData.groupByAttribute?.toString(),
unit,
};
const chart: Chart = {
id: index.toString(),
type: chartType,
@@ -209,6 +240,7 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
queryConfig.metricQueryData.filterData.metricName?.toString() ||
"",
description: queryConfig.metricAliasData?.description || "",
metricInfo,
props: {
data: chartSeries,
xAxis: {

View File

@@ -0,0 +1,590 @@
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 { 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";
interface ServiceMetricSummary {
service: Service;
metricCount: number;
metricNames: Array<string>;
metricUnits: Array<string>;
metricDescriptions: Array<string>;
hasSystemMetrics: boolean;
hasAppMetrics: boolean;
}
interface MetricCategory {
name: string;
count: number;
color: string;
bgColor: string;
}
const MetricsDashboard: FunctionComponent = (): ReactElement => {
const [serviceSummaries, setServiceSummaries] = useState<
Array<ServiceMetricSummary>
>([]);
const [totalMetricCount, setTotalMetricCount] = useState<number>(0);
const [metricCategories, setMetricCategories] = useState<
Array<MetricCategory>
>([]);
const [servicesWithNoMetrics, setServicesWithNoMetrics] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const categorizeMetric: (name: string) => string = (name: string): string => {
const lower: string = name.toLowerCase();
if (
lower.includes("cpu") ||
lower.includes("memory") ||
lower.includes("disk") ||
lower.includes("network") ||
lower.includes("system") ||
lower.includes("process") ||
lower.includes("runtime") ||
lower.includes("gc")
) {
return "System";
}
if (
lower.includes("http") ||
lower.includes("request") ||
lower.includes("response") ||
lower.includes("latency") ||
lower.includes("duration") ||
lower.includes("rpc")
) {
return "Request";
}
if (
lower.includes("db") ||
lower.includes("database") ||
lower.includes("query") ||
lower.includes("connection") ||
lower.includes("pool")
) {
return "Database";
}
if (
lower.includes("queue") ||
lower.includes("message") ||
lower.includes("kafka") ||
lower.includes("rabbit") ||
lower.includes("publish") ||
lower.includes("consume")
) {
return "Messaging";
}
return "Custom";
};
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
try {
setIsLoading(true);
setError("");
// Load services and metrics in parallel
const [servicesResult, metricsResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
ModelAPI.getList({
modelType: MetricType,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
name: true,
unit: true,
description: true,
services: {
_id: true,
name: true,
serviceColor: true,
} as any,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
]);
const services: Array<Service> = servicesResult.data || [];
const metrics: Array<MetricType> = metricsResult.data || [];
setTotalMetricCount(metrics.length);
// Build category counts
const categoryMap: Map<string, number> = new Map();
for (const metric of metrics) {
const cat: string = categorizeMetric(metric.name || "");
categoryMap.set(cat, (categoryMap.get(cat) || 0) + 1);
}
const categoryColors: Record<string, { color: string; bgColor: string }> =
{
System: { color: "text-blue-700", bgColor: "bg-blue-50" },
Request: { color: "text-purple-700", bgColor: "bg-purple-50" },
Database: { color: "text-amber-700", bgColor: "bg-amber-50" },
Messaging: { color: "text-green-700", bgColor: "bg-green-50" },
Custom: { color: "text-gray-700", bgColor: "bg-gray-50" },
};
const categories: Array<MetricCategory> = Array.from(
categoryMap.entries(),
)
.map(([name, count]: [string, number]) => {
return {
name,
count,
color: categoryColors[name]?.color || "text-gray-700",
bgColor: categoryColors[name]?.bgColor || "bg-gray-50",
};
})
.sort((a: MetricCategory, b: MetricCategory) => {
return b.count - a.count;
});
setMetricCategories(categories);
// 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: [],
metricUnits: [],
metricDescriptions: [],
hasSystemMetrics: false,
hasAppMetrics: false,
});
}
for (const metric of metrics) {
const metricServices: Array<Service> = metric.services || [];
const cat: string = categorizeMetric(metric.name || "");
for (const metricService of metricServices) {
const serviceId: string =
metricService._id?.toString() || metricService.id?.toString() || "";
let summary: ServiceMetricSummary | undefined =
summaryMap.get(serviceId);
if (!summary) {
summary = {
service: metricService,
metricCount: 0,
metricNames: [],
metricUnits: [],
metricDescriptions: [],
hasSystemMetrics: false,
hasAppMetrics: false,
};
summaryMap.set(serviceId, summary);
}
summary.metricCount += 1;
if (cat === "System") {
summary.hasSystemMetrics = true;
} else {
summary.hasAppMetrics = true;
}
const metricName: string = metric.name || "";
if (metricName && summary.metricNames.length < 6) {
summary.metricNames.push(metricName);
}
}
}
const summariesWithData: Array<ServiceMetricSummary> = Array.from(
summaryMap.values(),
).filter((s: ServiceMetricSummary) => {
return s.metricCount > 0;
});
const noMetricsCount: number = services.length - summariesWithData.length;
setServicesWithNoMetrics(noMetricsCount);
// 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-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
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-semibold text-gray-900 mb-2">
No metrics data yet
</h3>
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending metrics via OpenTelemetry, you{"'"}ll
see coverage, categories, and per-service breakdowns here.
</p>
</div>
);
}
const maxMetrics: number = Math.max(
...serviceSummaries.map((s: ServiceMetricSummary) => {
return s.metricCount;
}),
);
return (
<Fragment>
{/* Hero Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Total Metrics</p>
<div className="h-9 w-9 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-indigo-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
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>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalMetricCount}
</p>
<p className="text-xs text-gray-400 mt-1">unique metric types</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">
Services Reporting
</p>
<div className="h-9 w-9 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length}
</p>
<p className="text-xs text-gray-400 mt-1">actively sending data</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Avg per Service</p>
<div className="h-9 w-9 rounded-lg bg-blue-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length > 0
? Math.round(totalMetricCount / serviceSummaries.length)
: 0}
</p>
<p className="text-xs text-gray-400 mt-1">metrics per service</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">No Metrics</p>
<div
className={`h-9 w-9 rounded-lg flex items-center justify-center ${servicesWithNoMetrics > 0 ? "bg-amber-50" : "bg-gray-50"}`}
>
<svg
className={`h-4.5 w-4.5 ${servicesWithNoMetrics > 0 ? "text-amber-600" : "text-gray-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div>
</div>
<p
className={`text-3xl font-bold mt-2 ${servicesWithNoMetrics > 0 ? "text-amber-600" : "text-gray-900"}`}
>
{servicesWithNoMetrics}
</p>
<p className="text-xs text-gray-400 mt-1">
{servicesWithNoMetrics > 0
? "services not instrumented"
: "all services covered"}
</p>
</div>
</div>
{/* Metric Categories */}
{metricCategories.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-5 mb-6">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Metric Categories
</h3>
<div className="flex flex-wrap gap-3">
{metricCategories.map((cat: MetricCategory) => {
const pct: number =
totalMetricCount > 0
? Math.round((cat.count / totalMetricCount) * 100)
: 0;
return (
<div
key={cat.name}
className={`flex items-center gap-2.5 px-3.5 py-2 rounded-lg ${cat.bgColor}`}
>
<span className={`text-sm font-semibold ${cat.color}`}>
{cat.count}
</span>
<span className={`text-sm ${cat.color}`}>{cat.name}</span>
<span className={`text-xs ${cat.color} opacity-60`}>
{pct}%
</span>
</div>
);
})}
</div>
{/* Category distribution bar */}
<div className="flex h-2 rounded-full overflow-hidden mt-3">
{metricCategories.map((cat: MetricCategory) => {
const pct: number =
totalMetricCount > 0 ? (cat.count / totalMetricCount) * 100 : 0;
const barColorMap: Record<string, string> = {
System: "bg-blue-400",
Request: "bg-purple-400",
Database: "bg-amber-400",
Messaging: "bg-green-400",
Custom: "bg-gray-300",
};
return (
<div
key={cat.name}
className={`${barColorMap[cat.name] || "bg-gray-300"}`}
style={{ width: `${Math.max(pct, 1)}%` }}
/>
);
})}
</div>
</div>
)}
{/* Service Cards */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-base font-semibold text-gray-900">
Services Reporting Metrics
</h3>
<p className="text-sm text-gray-500 mt-0.5">
Coverage and instrumentation per service
</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) => {
const coverage: number =
maxMetrics > 0
? Math.round((summary.metricCount / maxMetrics) * 100)
: 0;
return (
<AppLink
key={
summary.service.id?.toString() ||
summary.service._id?.toString()
}
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_METRICS] as Route,
{
modelId: new ObjectID(
(summary.service._id as string) ||
summary.service.id?.toString() ||
"",
),
},
)}
>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-indigo-200 hover:shadow-md transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<ServiceElement service={summary.service} />
<div className="flex items-center gap-1.5">
{summary.hasSystemMetrics && (
<span className="text-xs bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full font-medium">
System
</span>
)}
{summary.hasAppMetrics && (
<span className="text-xs bg-purple-50 text-purple-700 px-2 py-0.5 rounded-full font-medium">
App
</span>
)}
</div>
</div>
{/* Metric count with relative bar */}
<div className="mb-4">
<div className="flex items-end justify-between mb-1.5">
<span className="text-2xl font-bold text-gray-900">
{summary.metricCount}
</span>
<span className="text-xs text-gray-400 mb-1">
metrics
</span>
</div>
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-indigo-400 transition-all duration-500"
style={{ width: `${Math.max(coverage, 3)}%` }}
/>
</div>
</div>
{/* Metric name tags */}
<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 text-xs font-medium bg-gray-50 text-gray-600 border border-gray-100"
>
{name}
</span>
);
})}
{summary.metricCount > summary.metricNames.length && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-50 text-gray-400">
+{summary.metricCount - summary.metricNames.length} more
</span>
)}
</div>
</div>
</AppLink>
);
})}
</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

@@ -12,7 +12,6 @@ import URL from "Common/Types/API/URL";
import { APP_API_URL } from "Common/UI/Config";
import AggregatedModel from "Common/Types/BaseDatabase/AggregatedModel";
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
import Dictionary from "Common/Types/Dictionary";
import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import OneUptimeDate from "Common/Types/Date";
@@ -36,7 +35,7 @@ export default class MetricUtil {
time: metricViewData.startAndEndDate!,
name: queryConfig.metricQueryData.filterData.metricName!,
attributes: queryConfig.metricQueryData.filterData
.attributes as Dictionary<string | number | boolean>,
.attributes as any,
},
aggregationType:
(queryConfig.metricQueryData.filterData

View File

@@ -0,0 +1,126 @@
import React, {
FunctionComponent,
ReactElement,
useCallback,
useState,
} from "react";
import ObjectID from "Common/Types/ObjectID";
import AlertMetricType from "Common/Types/Alerts/AlertMetricType";
import AlertMetricTypeUtil from "Common/Utils/Alerts/AlertMetricType";
import MetricView from "../Metrics/MetricView";
import ProjectUtil from "Common/UI/Utils/Project";
import MetricQueryConfigData, {
MetricChartType,
} from "Common/Types/Metrics/MetricQueryConfigData";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import RangeStartAndEndDateTime, {
RangeStartAndEndDateTimeUtil,
} from "Common/Types/Time/RangeStartAndEndDateTime";
import TimeRange from "Common/Types/Time/TimeRange";
import RangeStartAndEndDateView from "Common/UI/Components/Date/RangeStartAndEndDateView";
import Card from "Common/UI/Components/Card/Card";
export interface ComponentProps {
monitorId: ObjectID;
}
const MonitorAlertMetrics: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const alertMetricTypes: Array<AlertMetricType> =
AlertMetricTypeUtil.getAllAlertMetricTypes();
const [timeRange, setTimeRange] = useState<RangeStartAndEndDateTime>({
range: TimeRange.PAST_ONE_DAY,
});
type GetQueryConfigsFunction = () => Array<MetricQueryConfigData>;
const getQueryConfigs: GetQueryConfigsFunction =
(): Array<MetricQueryConfigData> => {
const queries: Array<MetricQueryConfigData> = [];
for (const metricType of alertMetricTypes) {
queries.push({
metricAliasData: {
metricVariable: metricType,
title: AlertMetricTypeUtil.getTitleByAlertMetricType(metricType),
description:
AlertMetricTypeUtil.getDescriptionByAlertMetricType(metricType),
legend: AlertMetricTypeUtil.getLegendByAlertMetricType(metricType),
legendUnit:
AlertMetricTypeUtil.getLegendUnitByAlertMetricType(metricType),
},
metricQueryData: {
filterData: {
metricName: metricType,
attributes: {
monitorId: props.monitorId.toString(),
projectId: ProjectUtil.getCurrentProjectId()?.toString() || "",
},
aggegationType:
AlertMetricTypeUtil.getAggregationTypeByAlertMetricType(
metricType,
),
},
groupBy: undefined,
},
chartType: MetricChartType.BAR,
});
}
return queries;
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate({
range: TimeRange.PAST_ONE_DAY,
}),
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
const handleTimeRangeChange: (
newTimeRange: RangeStartAndEndDateTime,
) => void = useCallback((newTimeRange: RangeStartAndEndDateTime): void => {
setTimeRange(newTimeRange);
const dateRange: InBetween<Date> =
RangeStartAndEndDateTimeUtil.getStartAndEndDate(newTimeRange);
setMetricViewData((prev: MetricViewData) => {
return {
...prev,
startAndEndDate: dateRange,
};
});
}, []);
return (
<Card
title="Alert Metrics"
description="Alert metrics for this monitor - count, time to acknowledge, time to resolve, and duration."
rightElement={
<RangeStartAndEndDateView
dashboardStartAndEndDate={timeRange}
onChange={handleTimeRangeChange}
/>
}
>
<MetricView
data={metricViewData}
hideQueryElements={true}
hideStartAndEndDate={true}
hideCardInCharts={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
}}
/>
</Card>
);
};
export default MonitorAlertMetrics;

View File

@@ -0,0 +1,222 @@
import React, {
FunctionComponent,
ReactElement,
useCallback,
useEffect,
useState,
} from "react";
import ObjectID from "Common/Types/ObjectID";
import MetricView from "../Metrics/MetricView";
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 { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import RangeStartAndEndDateTime, {
RangeStartAndEndDateTimeUtil,
} from "Common/Types/Time/RangeStartAndEndDateTime";
import TimeRange from "Common/Types/Time/TimeRange";
import RangeStartAndEndDateView from "Common/UI/Components/Date/RangeStartAndEndDateView";
import Card from "Common/UI/Components/Card/Card";
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
import IconProp from "Common/Types/Icon/IconProp";
import Metric from "Common/Models/AnalyticsModels/Metric";
import AnalyticsModelAPI, {
ListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import OneUptimeDate from "Common/Types/Date";
import Search from "Common/Types/BaseDatabase/Search";
export interface ComponentProps {
monitorId: ObjectID;
}
const MonitorCustomMetrics: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [customMetricNames, setCustomMetricNames] = useState<Array<string>>([]);
const fetchCustomMetricNames: PromiseVoidFunction =
async (): Promise<void> => {
setIsLoading(true);
try {
/*
* Query ClickHouse for recent metrics belonging to this monitor
* with names starting with "custom.monitor."
* monitorId is stored as serviceId in the Metric table.
*/
const listResult: ListResult<Metric> =
await AnalyticsModelAPI.getList<Metric>({
modelType: Metric,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
serviceId: props.monitorId,
name: new Search("custom.monitor.") as any,
time: new InBetween(
OneUptimeDate.addRemoveDays(
OneUptimeDate.getCurrentDate(),
-30,
),
OneUptimeDate.getCurrentDate(),
) as any,
},
select: {
name: true,
},
limit: 1000,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
// Extract distinct metric names
const nameSet: Set<string> = new Set<string>();
for (const metric of listResult.data) {
const name: string = (metric as any).name || "";
if (name.length > 0) {
nameSet.add(name);
}
}
const names: Array<string> = Array.from(nameSet).sort();
setCustomMetricNames(names);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCustomMetricNames().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
const [timeRange, setTimeRange] = useState<RangeStartAndEndDateTime>({
range: TimeRange.PAST_ONE_HOUR,
});
const getQueryConfigs: () => Array<MetricQueryConfigData> =
(): Array<MetricQueryConfigData> => {
return customMetricNames.map(
(metricName: string): MetricQueryConfigData => {
const displayName: string = metricName.replace("custom.monitor.", "");
return {
metricAliasData: {
metricVariable: metricName,
title: displayName,
description: `Custom metric: ${displayName}`,
legend: displayName,
legendUnit: "",
},
metricQueryData: {
filterData: {
metricName: metricName,
attributes: {
monitorId: props.monitorId.toString(),
projectId:
ProjectUtil.getCurrentProjectId()?.toString() || "",
},
aggegationType: AggregationType.Avg,
},
groupBy: {
attributes: true,
},
},
};
},
);
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate({
range: TimeRange.PAST_ONE_HOUR,
}),
queryConfigs: [],
formulaConfigs: [],
});
useEffect(() => {
if (customMetricNames.length > 0) {
setMetricViewData({
startAndEndDate:
RangeStartAndEndDateTimeUtil.getStartAndEndDate(timeRange),
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
}
}, [customMetricNames]);
const handleTimeRangeChange: (
newTimeRange: RangeStartAndEndDateTime,
) => void = useCallback((newTimeRange: RangeStartAndEndDateTime): void => {
setTimeRange(newTimeRange);
const dateRange: InBetween<Date> =
RangeStartAndEndDateTimeUtil.getStartAndEndDate(newTimeRange);
setMetricViewData((prev: MetricViewData) => {
return {
...prev,
startAndEndDate: dateRange,
};
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (customMetricNames.length === 0) {
return (
<EmptyState
id="no-custom-metrics"
icon={IconProp.ChartBar}
title="No Custom Metrics"
description="No custom metrics have been captured yet. Use oneuptime.captureMetric() in your monitor script to capture custom metrics."
/>
);
}
return (
<Card
title="Custom Metrics"
description="Custom metrics captured from your monitor script using oneuptime.captureMetric()."
rightElement={
<RangeStartAndEndDateView
dashboardStartAndEndDate={timeRange}
onChange={handleTimeRangeChange}
/>
}
>
<MetricView
data={metricViewData}
hideQueryElements={true}
hideStartAndEndDate={true}
hideCardInCharts={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
}}
/>
</Card>
);
};
export default MonitorCustomMetrics;

View File

@@ -0,0 +1,133 @@
import React, {
FunctionComponent,
ReactElement,
useCallback,
useState,
} from "react";
import ObjectID from "Common/Types/ObjectID";
import Search from "Common/Types/BaseDatabase/Search";
import IncidentMetricType from "Common/Types/Incident/IncidentMetricType";
import IncidentMetricTypeUtil from "Common/Utils/Incident/IncidentMetricType";
import MetricView from "../Metrics/MetricView";
import ProjectUtil from "Common/UI/Utils/Project";
import MetricQueryConfigData, {
MetricChartType,
} from "Common/Types/Metrics/MetricQueryConfigData";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import RangeStartAndEndDateTime, {
RangeStartAndEndDateTimeUtil,
} from "Common/Types/Time/RangeStartAndEndDateTime";
import TimeRange from "Common/Types/Time/TimeRange";
import RangeStartAndEndDateView from "Common/UI/Components/Date/RangeStartAndEndDateView";
import Card from "Common/UI/Components/Card/Card";
export interface ComponentProps {
monitorId: ObjectID;
}
const MonitorIncidentMetrics: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const incidentMetricTypes: Array<IncidentMetricType> =
IncidentMetricTypeUtil.getAllIncidentMetricTypes();
const [timeRange, setTimeRange] = useState<RangeStartAndEndDateTime>({
range: TimeRange.PAST_ONE_DAY,
});
type GetQueryConfigsFunction = () => Array<MetricQueryConfigData>;
const getQueryConfigs: GetQueryConfigsFunction =
(): Array<MetricQueryConfigData> => {
const queries: Array<MetricQueryConfigData> = [];
for (const metricType of incidentMetricTypes) {
queries.push({
metricAliasData: {
metricVariable: metricType,
title:
IncidentMetricTypeUtil.getTitleByIncidentMetricType(metricType),
description:
IncidentMetricTypeUtil.getDescriptionByIncidentMetricType(
metricType,
),
legend:
IncidentMetricTypeUtil.getLegendByIncidentMetricType(metricType),
legendUnit:
IncidentMetricTypeUtil.getLegendUnitByIncidentMetricType(
metricType,
),
},
metricQueryData: {
filterData: {
metricName: metricType,
attributes: {
monitorIds: new Search(props.monitorId.toString()),
projectId: ProjectUtil.getCurrentProjectId()?.toString() || "",
},
aggegationType:
IncidentMetricTypeUtil.getAggregationTypeByIncidentMetricType(
metricType,
),
},
groupBy: undefined,
},
chartType: MetricChartType.BAR,
});
}
return queries;
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate({
range: TimeRange.PAST_ONE_DAY,
}),
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
const handleTimeRangeChange: (
newTimeRange: RangeStartAndEndDateTime,
) => void = useCallback((newTimeRange: RangeStartAndEndDateTime): void => {
setTimeRange(newTimeRange);
const dateRange: InBetween<Date> =
RangeStartAndEndDateTimeUtil.getStartAndEndDate(newTimeRange);
setMetricViewData((prev: MetricViewData) => {
return {
...prev,
startAndEndDate: dateRange,
};
});
}, []);
return (
<Card
title="Incident Metrics"
description="Incident metrics for this monitor - count, time to acknowledge, time to resolve, and duration."
rightElement={
<RangeStartAndEndDateView
dashboardStartAndEndDate={timeRange}
onChange={handleTimeRangeChange}
/>
}
>
<MetricView
data={metricViewData}
hideQueryElements={true}
hideStartAndEndDate={true}
hideCardInCharts={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: getQueryConfigs(),
formulaConfigs: [],
});
}}
/>
</Card>
);
};
export default MonitorIncidentMetrics;

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,621 @@
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 { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Profile from "Common/Models/AnalyticsModels/Profile";
import AnalyticsModelAPI 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>;
totalSamples: number;
}
interface FunctionHotspot {
functionName: string;
fileName: string;
selfValue: number;
totalValue: number;
sampleCount: number;
frameType: string;
}
interface ProfileTypeStats {
type: string;
count: number;
displayName: string;
badgeColor: string;
}
const ProfilesDashboard: FunctionComponent = (): ReactElement => {
const [serviceSummaries, setServiceSummaries] = useState<
Array<ServiceProfileSummary>
>([]);
const [hotspots, setHotspots] = useState<Array<FunctionHotspot>>([]);
const [profileTypeStats, setProfileTypeStats] = useState<
Array<ProfileTypeStats>
>([]);
const [totalProfileCount, setTotalProfileCount] = useState<number>(0);
const [totalSampleCount, setTotalSampleCount] = 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("");
const now: Date = OneUptimeDate.getCurrentDate();
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
const [servicesResult, profilesResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
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 services: Array<Service> = servicesResult.data || [];
const profiles: Array<Profile> = profilesResult.data || [];
setTotalProfileCount(profiles.length);
// Build per-service summaries
const summaryMap: Map<string, ServiceProfileSummary> = new Map();
const typeCountMap: Map<string, number> = new Map();
let totalSamples: number = 0;
for (const service of services) {
const serviceId: string = service.id?.toString() || "";
summaryMap.set(serviceId, {
service,
profileCount: 0,
latestProfileTime: null,
profileTypes: [],
totalSamples: 0,
});
}
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 samples: number = (profile.sampleCount as number) || 0;
summary.totalSamples += samples;
totalSamples += samples;
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);
}
// Track global type stats
typeCountMap.set(profileType, (typeCountMap.get(profileType) || 0) + 1);
}
setTotalSampleCount(totalSamples);
// Build profile type stats
const typeStats: Array<ProfileTypeStats> = Array.from(
typeCountMap.entries(),
)
.map(([type, count]: [string, number]) => {
return {
type,
count,
displayName: ProfileUtil.getProfileTypeDisplayName(type),
badgeColor: ProfileUtil.getProfileTypeBadgeColor(type),
};
})
.sort((a: ProfileTypeStats, b: ProfileTypeStats) => {
return b.count - a.count;
});
setProfileTypeStats(typeStats);
// Only show services that have profiles
const summariesWithData: Array<ServiceProfileSummary> = Array.from(
summaryMap.values(),
).filter((s: ServiceProfileSummary) => {
return s.profileCount > 0;
});
summariesWithData.sort(
(a: ServiceProfileSummary, b: ServiceProfileSummary) => {
return b.profileCount - a.profileCount;
},
);
setServiceSummaries(summariesWithData);
// Load top hotspots
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 {
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-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No profiling data yet
</h3>
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending profiling data, you{"'"}ll see
performance hotspots, resource usage patterns, and optimization
opportunities.
</p>
</div>
);
}
const maxProfiles: number = Math.max(
...serviceSummaries.map((s: ServiceProfileSummary) => {
return s.profileCount;
}),
);
return (
<Fragment>
{/* Hero Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Profiles</p>
<div className="h-9 w-9 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-indigo-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalProfileCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">last hour</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Services</p>
<div className="h-9 w-9 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length}
</p>
<p className="text-xs text-gray-400 mt-1">being profiled</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Samples</p>
<div className="h-9 w-9 rounded-lg bg-blue-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalSampleCount >= 1_000_000
? `${(totalSampleCount / 1_000_000).toFixed(1)}M`
: totalSampleCount >= 1_000
? `${(totalSampleCount / 1_000).toFixed(1)}K`
: totalSampleCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">total samples</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Hotspots</p>
<div
className={`h-9 w-9 rounded-lg flex items-center justify-center ${hotspots.length > 0 ? "bg-orange-50" : "bg-gray-50"}`}
>
<svg
className={`h-4.5 w-4.5 ${hotspots.length > 0 ? "text-orange-600" : "text-gray-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 6.51 6.51 0 009 4.572c.163.07.322.148.476.232M12 18.75a6.743 6.743 0 002.14-1.234M12 18.75a6.72 6.72 0 01-2.14-1.234M12 18.75V21m-4.773-4.227l-1.591 1.591M5.636 5.636L4.045 4.045m0 15.91l1.591-1.591M18.364 5.636l1.591-1.591M21 12h-2.25M4.5 12H2.25"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{hotspots.length}
</p>
<p className="text-xs text-gray-400 mt-1">functions identified</p>
</div>
</div>
{/* Profile Type Distribution */}
{profileTypeStats.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-5 mb-6">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Profile Types Collected
</h3>
<div className="flex flex-wrap gap-3">
{profileTypeStats.map((stat: ProfileTypeStats) => {
const pct: number =
totalProfileCount > 0
? Math.round((stat.count / totalProfileCount) * 100)
: 0;
return (
<div
key={stat.type}
className={`flex items-center gap-2.5 px-3.5 py-2 rounded-lg ${stat.badgeColor}`}
>
<span className="text-sm font-semibold">{stat.count}</span>
<span className="text-sm">{stat.displayName}</span>
<span className="text-xs opacity-60">{pct}%</span>
</div>
);
})}
</div>
</div>
)}
{/* Service Cards */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-base font-semibold text-gray-900">
Services Being Profiled
</h3>
<p className="text-sm text-gray-500 mt-0.5">
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) => {
const coverage: number =
maxProfiles > 0
? Math.round((summary.profileCount / maxProfiles) * 100)
: 0;
return (
<AppLink
key={summary.service.id?.toString()}
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_PROFILES] as Route,
{
modelId: new ObjectID(summary.service._id as string),
},
)}
>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-indigo-200 hover:shadow-md transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<ServiceElement service={summary.service} />
<span className="text-xs bg-green-50 text-green-700 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500 mb-0.5">Profiles</p>
<p className="text-xl font-bold text-gray-900">
{summary.profileCount}
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-0.5">Samples</p>
<p className="text-xl font-bold text-gray-900">
{summary.totalSamples >= 1_000
? `${(summary.totalSamples / 1_000).toFixed(1)}K`
: summary.totalSamples.toLocaleString()}
</p>
</div>
</div>
{/* Profile volume bar */}
<div className="mb-3">
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-indigo-400 transition-all duration-500"
style={{ width: `${Math.max(coverage, 3)}%` }}
/>
</div>
</div>
{/* Profile type badges */}
<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 text-xs font-medium ${badgeColor}`}
>
{ProfileUtil.getProfileTypeDisplayName(profileType)}
</span>
);
})}
</div>
{summary.latestProfileTime && (
<p className="text-xs text-gray-400 mt-3">
Last captured{" "}
{OneUptimeDate.fromNow(summary.latestProfileTime)}
</p>
)}
</div>
</AppLink>
);
})}
</div>
</div>
{/* Top Hotspots */}
{hotspots.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<h3 className="text-base font-semibold text-gray-900">
Top Performance Hotspots
</h3>
</div>
<p className="text-sm text-gray-500 mb-4">
Functions consuming the most resources across all services
</p>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{hotspots.map((fn: FunctionHotspot, index: number) => {
const maxSelf: number = hotspots[0]?.selfValue || 1;
const barWidth: number = (fn.selfValue / maxSelf) * 100;
return (
<div
key={`${fn.functionName}-${fn.fileName}-${index}`}
className="px-5 py-3.5 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-1.5">
<div className="min-w-0 flex-1 mr-4">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 font-mono w-5 flex-shrink-0">
#{index + 1}
</span>
<p className="font-mono text-sm text-gray-900 truncate">
{fn.functionName}
</p>
{fn.frameType && (
<span className="flex-shrink-0 text-xs px-1.5 py-0.5 rounded font-medium bg-gray-100 text-gray-600">
{fn.frameType}
</span>
)}
</div>
{fn.fileName && (
<p className="text-xs text-gray-400 mt-0.5 ml-7 truncate">
{fn.fileName}
</p>
)}
</div>
<div className="flex items-center gap-5 flex-shrink-0">
<div className="text-right">
<p className="text-sm font-bold font-mono text-gray-900">
{fn.selfValue.toLocaleString()}
</p>
<p className="text-xs text-gray-400">own time</p>
</div>
<div className="text-right">
<p className="text-sm font-mono text-gray-700">
{fn.totalValue.toLocaleString()}
</p>
<p className="text-xs text-gray-400">total</p>
</div>
<div className="text-right">
<p className="text-sm font-mono text-gray-700">
{fn.sampleCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400">samples</p>
</div>
</div>
</div>
<div className="ml-7">
<div className="w-full h-1 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-orange-400"
style={{ width: `${barWidth}%` }}
/>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
</Fragment>
);
};
export default ProfilesDashboard;

View File

@@ -86,9 +86,11 @@ function replacePlaceholders(
otlpUrl: string,
otlpHost: string,
token: string,
pyroscopeUrl: string,
): string {
return code
.replace(/<YOUR_ONEUPTIME_OTLP_URL>/g, otlpUrl)
.replace(/<YOUR_ONEUPTIME_URL>/g, otlpUrl)
.replace(/<YOUR_ONEUPTIME_PYROSCOPE_URL>/g, pyroscopeUrl)
.replace(/<YOUR_ONEUPTIME_OTLP_HOST>/g, otlpHost)
.replace(/<YOUR_ONEUPTIME_TOKEN>/g, token);
}
@@ -261,19 +263,19 @@ import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
const sdk = new NodeSDK({
serviceName: 'my-service',
traceExporter: new OTLPTraceExporter({
url: '<YOUR_ONEUPTIME_OTLP_URL>/v1/traces',
url: '<YOUR_ONEUPTIME_URL>/v1/traces',
headers: { 'x-oneuptime-token': '<YOUR_ONEUPTIME_TOKEN>' },
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: '<YOUR_ONEUPTIME_OTLP_URL>/v1/metrics',
url: '<YOUR_ONEUPTIME_URL>/v1/metrics',
headers: { 'x-oneuptime-token': '<YOUR_ONEUPTIME_TOKEN>' },
}),
}),
logRecordProcessors: [
new BatchLogRecordProcessor(
new OTLPLogExporter({
url: '<YOUR_ONEUPTIME_OTLP_URL>/v1/logs',
url: '<YOUR_ONEUPTIME_URL>/v1/logs',
headers: { 'x-oneuptime-token': '<YOUR_ONEUPTIME_TOKEN>' },
})
),
@@ -303,7 +305,7 @@ trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="<YOUR_ONEUPTIME_OTLP_URL>",
endpoint="<YOUR_ONEUPTIME_URL>",
headers={"x-oneuptime-token": "<YOUR_ONEUPTIME_TOKEN>"},
)
)
@@ -313,7 +315,7 @@ trace.set_tracer_provider(trace_provider)
# Metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(
endpoint="<YOUR_ONEUPTIME_OTLP_URL>",
endpoint="<YOUR_ONEUPTIME_URL>",
headers={"x-oneuptime-token": "<YOUR_ONEUPTIME_TOKEN>"},
)
)
@@ -363,7 +365,7 @@ func initTracer() (*sdktrace.TracerProvider, error) {
code: `# Run your Java application with the OpenTelemetry agent:
java -javaagent:opentelemetry-javaagent.jar \\
-Dotel.service.name=my-service \\
-Dotel.exporter.otlp.endpoint=<YOUR_ONEUPTIME_OTLP_URL> \\
-Dotel.exporter.otlp.endpoint=<YOUR_ONEUPTIME_URL> \\
-Dotel.exporter.otlp.headers="x-oneuptime-token=<YOUR_ONEUPTIME_TOKEN>" \\
-Dotel.exporter.otlp.protocol=http/protobuf \\
-Dotel.metrics.exporter=otlp \\
@@ -393,7 +395,7 @@ builder.Services.AddOpenTelemetry()
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(options => {
options.Endpoint = new Uri("<YOUR_ONEUPTIME_OTLP_URL>");
options.Endpoint = new Uri("<YOUR_ONEUPTIME_URL>");
options.Headers = "x-oneuptime-token=<YOUR_ONEUPTIME_TOKEN>";
options.Protocol = OtlpExportProtocol.HttpProtobuf;
})
@@ -402,7 +404,7 @@ builder.Services.AddOpenTelemetry()
.SetResourceBuilder(resourceBuilder)
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(options => {
options.Endpoint = new Uri("<YOUR_ONEUPTIME_OTLP_URL>");
options.Endpoint = new Uri("<YOUR_ONEUPTIME_URL>");
options.Headers = "x-oneuptime-token=<YOUR_ONEUPTIME_TOKEN>";
options.Protocol = OtlpExportProtocol.HttpProtobuf;
})
@@ -412,7 +414,7 @@ builder.Services.AddOpenTelemetry()
builder.Logging.AddOpenTelemetry(logging => {
logging.SetResourceBuilder(resourceBuilder);
logging.AddOtlpExporter(options => {
options.Endpoint = new Uri("<YOUR_ONEUPTIME_OTLP_URL>");
options.Endpoint = new Uri("<YOUR_ONEUPTIME_URL>");
options.Headers = "x-oneuptime-token=<YOUR_ONEUPTIME_TOKEN>";
options.Protocol = OtlpExportProtocol.HttpProtobuf;
});
@@ -440,7 +442,7 @@ fn init_tracer() -> sdktrace::TracerProvider {
let exporter = opentelemetry_otlp::new_exporter()
.http()
.with_endpoint("<YOUR_ONEUPTIME_OTLP_URL>")
.with_endpoint("<YOUR_ONEUPTIME_URL>")
.with_headers(headers);
opentelemetry_otlp::new_pipeline()
@@ -471,7 +473,7 @@ use OpenTelemetry\\SemConv\\ResourceAttributes;
use OpenTelemetry\\Contrib\\Otlp\\HttpTransportFactory;
$transport = (new HttpTransportFactory())->create(
'<YOUR_ONEUPTIME_OTLP_URL>/v1/traces',
'<YOUR_ONEUPTIME_URL>/v1/traces',
'application/x-protobuf',
['x-oneuptime-token' => '<YOUR_ONEUPTIME_TOKEN>']
);
@@ -503,7 +505,7 @@ OpenTelemetry::SDK.configure do |c|
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: '<YOUR_ONEUPTIME_OTLP_URL>/v1/traces',
endpoint: '<YOUR_ONEUPTIME_URL>/v1/traces',
headers: { 'x-oneuptime-token' => '<YOUR_ONEUPTIME_TOKEN>' }
)
)
@@ -523,7 +525,7 @@ config :opentelemetry,
config :opentelemetry_exporter,
otlp_protocol: :http_protobuf,
otlp_endpoint: "<YOUR_ONEUPTIME_OTLP_URL>",
otlp_endpoint: "<YOUR_ONEUPTIME_URL>",
otlp_headers: [{"x-oneuptime-token", "<YOUR_ONEUPTIME_TOKEN>"}]
# In application.ex, add to children:
@@ -545,7 +547,7 @@ namespace otlp = opentelemetry::exporter::otlp;
void initTracer() {
otlp::OtlpHttpExporterOptions opts;
opts.url = "<YOUR_ONEUPTIME_OTLP_URL>/v1/traces";
opts.url = "<YOUR_ONEUPTIME_URL>/v1/traces";
opts.http_headers = {{"x-oneuptime-token", "<YOUR_ONEUPTIME_TOKEN>"}};
auto exporter = otlp::OtlpHttpExporterFactory::Create(opts);
@@ -573,7 +575,7 @@ import OtlpHttpSpanExporting
func initTracer() {
let exporter = OtlpHttpSpanExporter(
endpoint: URL(string: "<YOUR_ONEUPTIME_OTLP_URL>/v1/traces")!,
endpoint: URL(string: "<YOUR_ONEUPTIME_URL>/v1/traces")!,
config: OtlpConfiguration(
headers: [("x-oneuptime-token", "<YOUR_ONEUPTIME_TOKEN>")]
)
@@ -614,7 +616,7 @@ const provider = new WebTracerProvider({
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: '<YOUR_ONEUPTIME_OTLP_URL>/v1/traces',
url: '<YOUR_ONEUPTIME_URL>/v1/traces',
headers: { 'x-oneuptime-token': '<YOUR_ONEUPTIME_TOKEN>' },
})
)
@@ -664,7 +666,7 @@ const provider = new WebTracerProvider({
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: '<YOUR_ONEUPTIME_OTLP_URL>/v1/traces',
url: '<YOUR_ONEUPTIME_URL>/v1/traces',
headers: { 'x-oneuptime-token': '<YOUR_ONEUPTIME_TOKEN>' },
})
)
@@ -698,7 +700,7 @@ registerInstrumentations({
function getEnvVarSnippet(): string {
return `# Alternatively, configure via environment variables (works with any language):
export OTEL_SERVICE_NAME="my-service"
export OTEL_EXPORTER_OTLP_ENDPOINT="<YOUR_ONEUPTIME_OTLP_URL>"
export OTEL_EXPORTER_OTLP_ENDPOINT="<YOUR_ONEUPTIME_URL>"
export OTEL_EXPORTER_OTLP_HEADERS="x-oneuptime-token=<YOUR_ONEUPTIME_TOKEN>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"`;
}
@@ -785,7 +787,7 @@ function getProfileConfigSnippet(lang: Language): {
code: `const Pyroscope = require('@pyroscope/nodejs');
Pyroscope.init({
serverAddress: '<YOUR_ONEUPTIME_OTLP_URL>',
serverAddress: '<YOUR_ONEUPTIME_PYROSCOPE_URL>',
appName: 'my-service',
tags: {
region: process.env.REGION || 'default',
@@ -802,7 +804,7 @@ Pyroscope.start();`,
pyroscope.configure(
application_name="my-service",
server_address="<YOUR_ONEUPTIME_OTLP_URL>",
server_address="<YOUR_ONEUPTIME_PYROSCOPE_URL>",
sample_rate=100,
tags={
"region": "us-east-1",
@@ -829,7 +831,7 @@ func main() {
pyroscope.Start(pyroscope.Config{
ApplicationName: "my-service",
ServerAddress: "<YOUR_ONEUPTIME_OTLP_URL>",
ServerAddress: "<YOUR_ONEUPTIME_PYROSCOPE_URL>",
AuthToken: os.Getenv("ONEUPTIME_TOKEN"),
Tags: map[string]string{"hostname": os.Getenv("HOSTNAME")},
ProfileTypes: []pyroscope.ProfileType{
@@ -863,7 +865,7 @@ PyroscopeAgent.start(
.setApplicationName("my-service")
.setProfilingEvent(EventType.ITIMER)
.setFormat(Format.JFR)
.setServerAddress("<YOUR_ONEUPTIME_OTLP_URL>")
.setServerAddress("<YOUR_ONEUPTIME_PYROSCOPE_URL>")
.setAuthToken("<YOUR_ONEUPTIME_TOKEN>")
.build()
);
@@ -871,7 +873,7 @@ PyroscopeAgent.start(
// Option 2: Attach as Java agent (no code changes)
// java -javaagent:pyroscope.jar \\
// -Dpyroscope.application.name=my-service \\
// -Dpyroscope.server.address=<YOUR_ONEUPTIME_OTLP_URL> \\
// -Dpyroscope.server.address=<YOUR_ONEUPTIME_PYROSCOPE_URL> \\
// -Dpyroscope.auth.token=<YOUR_ONEUPTIME_TOKEN> \\
// -jar my-app.jar`,
language: "java",
@@ -880,7 +882,7 @@ PyroscopeAgent.start(
return {
code: `# Set environment variables before running your .NET application:
export PYROSCOPE_APPLICATION_NAME=my-service
export PYROSCOPE_SERVER_ADDRESS=<YOUR_ONEUPTIME_OTLP_URL>
export PYROSCOPE_SERVER_ADDRESS=<YOUR_ONEUPTIME_PYROSCOPE_URL>
export PYROSCOPE_AUTH_TOKEN=<YOUR_ONEUPTIME_TOKEN>
export PYROSCOPE_PROFILING_ENABLED=1
export CORECLR_ENABLE_PROFILING=1
@@ -899,7 +901,7 @@ require 'pyroscope'
Pyroscope.configure do |config|
config.application_name = "my-service"
config.server_address = "<YOUR_ONEUPTIME_OTLP_URL>"
config.server_address = "<YOUR_ONEUPTIME_PYROSCOPE_URL>"
config.auth_token = "<YOUR_ONEUPTIME_TOKEN>"
config.tags = {
"hostname" => ENV["HOSTNAME"],
@@ -918,7 +920,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend_impl = pprof_backend(pprof_config);
let agent = PyroscopeAgent::builder(
"<YOUR_ONEUPTIME_OTLP_URL>", "my-service"
"<YOUR_ONEUPTIME_PYROSCOPE_URL>", "my-service"
)
.backend(backend_impl)
.auth_token("<YOUR_ONEUPTIME_TOKEN>".to_string())
@@ -973,7 +975,7 @@ pyroscope.ebpf "default" {
pyroscope.write "oneuptime" {
endpoint {
url = "<YOUR_ONEUPTIME_OTLP_URL>"
url = "<YOUR_ONEUPTIME_PYROSCOPE_URL>"
headers = {
"x-oneuptime-token" = "<YOUR_ONEUPTIME_TOKEN>",
}
@@ -1110,7 +1112,10 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
const otlpHost: string = HOST ? HOST : "<YOUR_ONEUPTIME_OTLP_HOST>";
const otlpUrl: string = HOST
? `${httpProtocol}://${HOST}/otlp`
: "<YOUR_ONEUPTIME_OTLP_URL>";
: "<YOUR_ONEUPTIME_URL>";
const pyroscopeUrl: string = HOST
? `${httpProtocol}://${HOST}/pyroscope`
: "<YOUR_ONEUPTIME_PYROSCOPE_URL>";
// Fetch ingestion keys on mount
useEffect(() => {
@@ -1558,6 +1563,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language={configSnippet.language}
/>,
@@ -1575,6 +1581,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="bash"
/>,
@@ -1607,6 +1614,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="yaml"
/>,
@@ -1622,6 +1630,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="yaml"
/>,
@@ -1661,6 +1670,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="yaml"
/>,
@@ -1676,6 +1686,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="yaml"
/>,
@@ -1715,8 +1726,9 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="hcl"
language="nginx"
/>,
)}
@@ -1730,6 +1742,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
otlpUrlValue,
otlpHostValue,
tokenValue,
pyroscopeUrl,
)}
language="yaml"
/>,

View File

@@ -0,0 +1,726 @@
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 { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Span, { SpanStatus } from "Common/Models/AnalyticsModels/Span";
import AnalyticsModelAPI 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;
p50Nanos: number;
p95Nanos: number;
durations: Array<number>;
}
interface RecentTrace {
traceId: string;
name: string;
serviceId: string;
startTime: Date;
statusCode: SpanStatus;
durationNano: number;
}
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 getPercentile: (arr: Array<number>, p: number) => number = (
arr: Array<number>,
p: number,
): number => {
if (arr.length === 0) {
return 0;
}
const sorted: Array<number> = [...arr].sort((a: number, b: number) => {
return a - b;
});
const idx: number = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, idx)] || 0;
};
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 [totalRequests, setTotalRequests] = useState<number>(0);
const [totalErrors, setTotalErrors] = useState<number>(0);
const [globalP50, setGlobalP50] = useState<number>(0);
const [globalP95, setGlobalP95] = useState<number>(0);
const [globalP99, setGlobalP99] = 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("");
const now: Date = OneUptimeDate.getCurrentDate();
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
const [servicesResult, spansResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
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 loadedServices: Array<Service> = servicesResult.data || [];
setServices(loadedServices);
const allSpans: Array<Span> = spansResult.data || [];
// Build per-service summaries
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,
p50Nanos: 0,
p95Nanos: 0,
durations: [],
});
}
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();
const allDurations: Array<number> = [];
for (const span of allSpans) {
const serviceId: string = span.serviceId?.toString() || "";
const traceId: string = span.traceId?.toString() || "";
const duration: number = (span.durationUnixNano as number) || 0;
const summary: ServiceTraceSummary | undefined =
summaryMap.get(serviceId);
if (duration > 0) {
allDurations.push(duration);
}
if (summary) {
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 (duration > 0) {
summary.durations.push(duration);
}
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;
}
}
if (!seenTraceIds.has(traceId) && traceId) {
seenTraceIds.add(traceId);
allTraces.push({
traceId,
name: span.name?.toString() || "Unknown",
serviceId,
startTime: span.startTime ? new Date(span.startTime) : new Date(),
statusCode: span.statusCode || SpanStatus.Unset,
durationNano: duration,
});
}
if (
span.statusCode === SpanStatus.Error &&
traceId &&
!seenErrorTraceIds.has(traceId)
) {
seenErrorTraceIds.add(traceId);
errorTraces.push({
traceId,
name: span.name?.toString() || "Unknown",
serviceId,
startTime: span.startTime ? new Date(span.startTime) : new Date(),
statusCode: span.statusCode,
durationNano: duration,
});
}
}
// Compute global percentiles
setGlobalP50(getPercentile(allDurations, 50));
setGlobalP95(getPercentile(allDurations, 95));
setGlobalP99(getPercentile(allDurations, 99));
// Compute per-service percentiles and filter
const summariesWithData: Array<ServiceTraceSummary> = Array.from(
summaryMap.values(),
)
.filter((s: ServiceTraceSummary) => {
return s.totalTraces > 0;
})
.map((s: ServiceTraceSummary) => {
return {
...s,
p50Nanos: getPercentile(s.durations, 50),
p95Nanos: getPercentile(s.durations, 95),
};
});
// Sort: highest error rate first, then by total traces
summariesWithData.sort(
(a: ServiceTraceSummary, b: ServiceTraceSummary) => {
const aErrorRate: number =
a.totalTraces > 0 ? a.errorTraces / a.totalTraces : 0;
const bErrorRate: number =
b.totalTraces > 0 ? b.errorTraces / b.totalTraces : 0;
if (bErrorRate !== aErrorRate) {
return bErrorRate - aErrorRate;
}
return b.totalTraces - a.totalTraces;
},
);
let totalReqs: number = 0;
let totalErrs: number = 0;
for (const s of summariesWithData) {
totalReqs += s.totalTraces;
totalErrs += s.errorTraces;
}
setTotalRequests(totalReqs);
setTotalErrors(totalErrs);
setServiceSummaries(summariesWithData);
setRecentErrorTraces(errorTraces.slice(0, 8));
const slowTraces: Array<RecentTrace> = [...allTraces]
.sort((a: RecentTrace, b: RecentTrace) => {
return b.durationNano - a.durationNano;
})
.slice(0, 8);
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-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No trace data yet
</h3>
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending distributed tracing data, you{"'"}ll
see request rates, error rates, latency percentiles, and more.
</p>
</div>
);
}
const overallErrorRate: number =
totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0;
return (
<Fragment>
{/* Hero Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<p className="text-sm font-medium text-gray-500">Requests</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{totalRequests.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">last hour</p>
</div>
<div
className={`rounded-xl border p-5 ${overallErrorRate > 5 ? "border-red-200 bg-red-50" : overallErrorRate > 1 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">Error Rate</p>
<p
className={`text-3xl font-bold mt-1 ${overallErrorRate > 5 ? "text-red-600" : overallErrorRate > 1 ? "text-amber-600" : "text-green-600"}`}
>
{overallErrorRate.toFixed(1)}%
</p>
<p className="text-xs text-gray-400 mt-1">
{totalErrors.toLocaleString()} errors
</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<p className="text-sm font-medium text-gray-500">P50 Latency</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{formatDuration(globalP50)}
</p>
<p className="text-xs text-gray-400 mt-1">median</p>
</div>
<div
className={`rounded-xl border p-5 ${globalP95 > 1_000_000_000 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">P95 Latency</p>
<p
className={`text-3xl font-bold mt-1 ${globalP95 > 1_000_000_000 ? "text-amber-600" : "text-gray-900"}`}
>
{formatDuration(globalP95)}
</p>
<p className="text-xs text-gray-400 mt-1">95th percentile</p>
</div>
<div
className={`rounded-xl border p-5 ${globalP99 > 2_000_000_000 ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">P99 Latency</p>
<p
className={`text-3xl font-bold mt-1 ${globalP99 > 2_000_000_000 ? "text-red-600" : "text-gray-900"}`}
>
{formatDuration(globalP99)}
</p>
<p className="text-xs text-gray-400 mt-1">99th percentile</p>
</div>
</div>
{/* Service Health Table */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-base font-semibold text-gray-900">
Service Health
</h3>
<p className="text-sm text-gray-500 mt-0.5">
Sorted by error rate services needing attention first
</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="rounded-xl border border-gray-200 bg-white overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Service
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Requests
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Error Rate
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
P50
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
P95
</th>
<th className="text-center text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Status
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
&nbsp;
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{serviceSummaries.map((summary: ServiceTraceSummary) => {
const errorRate: number =
summary.totalTraces > 0
? (summary.errorTraces / summary.totalTraces) * 100
: 0;
let healthColor: string = "bg-green-500";
let healthLabel: string = "Healthy";
let healthBg: string = "bg-green-50 text-green-700";
if (errorRate > 10) {
healthColor = "bg-red-500";
healthLabel = "Critical";
healthBg = "bg-red-50 text-red-700";
} else if (errorRate > 5) {
healthColor = "bg-amber-500";
healthLabel = "Degraded";
healthBg = "bg-amber-50 text-amber-700";
} else if (errorRate > 1) {
healthColor = "bg-yellow-400";
healthLabel = "Warning";
healthBg = "bg-yellow-50 text-yellow-700";
}
return (
<tr
key={summary.service.id?.toString()}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-5 py-3.5">
<ServiceElement service={summary.service} />
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-medium text-gray-900">
{summary.totalTraces.toLocaleString()}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<div className="flex items-center justify-end gap-2">
<div className="w-16 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${errorRate > 10 ? "bg-red-500" : errorRate > 5 ? "bg-amber-400" : errorRate > 0 ? "bg-yellow-400" : "bg-green-400"}`}
style={{
width: `${Math.max(errorRate, errorRate > 0 ? 3 : 0)}%`,
}}
/>
</div>
<span
className={`text-sm font-medium ${errorRate > 5 ? "text-red-600" : "text-gray-900"}`}
>
{errorRate.toFixed(1)}%
</span>
</div>
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-mono text-gray-700">
{formatDuration(summary.p50Nanos)}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-mono text-gray-700">
{formatDuration(summary.p95Nanos)}
</span>
</td>
<td className="px-5 py-3.5 text-center">
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${healthBg}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${healthColor}`}
/>
{healthLabel}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<AppLink
className="text-xs 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
</AppLink>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Two-column: Errors + Slow Requests */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Errors */}
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500" />
<h3 className="text-base font-semibold text-gray-900">
Recent Errors
</h3>
{recentErrorTraces.length > 0 && (
<span className="text-xs bg-red-50 text-red-700 px-2 py-0.5 rounded-full font-medium">
{recentErrorTraces.length}
</span>
)}
</div>
</div>
{recentErrorTraces.length === 0 ? (
<div className="rounded-xl border border-gray-200 bg-white p-10 text-center">
<div className="mx-auto w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mb-3">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">
No errors in the last hour
</p>
<p className="text-xs text-gray-400 mt-1">Looking good!</p>
</div>
) : (
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{recentErrorTraces.map((trace: RecentTrace, index: number) => {
return (
<AppLink
key={`${trace.traceId}-${index}`}
className="block px-4 py-3 hover:bg-red-50/30 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.fromNow(trace.startTime)}
</p>
</div>
</div>
</AppLink>
);
})}
</div>
</div>
)}
</div>
{/* Slowest Requests */}
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-amber-500" />
<h3 className="text-base font-semibold text-gray-900">
Slowest Requests
</h3>
</div>
{recentSlowTraces.length === 0 ? (
<div className="rounded-xl border border-gray-200 bg-white p-10 text-center">
<p className="text-sm text-gray-500">
No traces in the last hour
</p>
</div>
) : (
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{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-amber-50/30 transition-colors"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.TRACE_VIEW]!,
{ modelId: trace.traceId },
)}
>
<div className="flex items-center justify-between mb-1.5">
<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

@@ -11,10 +11,7 @@ const MetricsViewLayout: FunctionComponent<
> = (): ReactElement => {
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
return (
<Page
title="Metrics Explorer"
breadcrumbLinks={getMetricsBreadcrumbs(path)}
>
<Page title="Metric Explorer" breadcrumbLinks={getMetricsBreadcrumbs(path)}>
<Outlet />
</Page>
);

View File

@@ -65,6 +65,10 @@ import MonitorFeedElement from "../../../Components/Monitor/MonitorFeed";
import URL from "Common/Types/API/URL";
import { APP_API_URL } from "Common/UI/Config";
import MonitorEvaluationSummary from "Common/Types/Monitor/MonitorEvaluationSummary";
import Incident from "Common/Models/DatabaseModels/Incident";
import UptimeBarTooltipIncident from "Common/Types/Monitor/UptimeBarTooltipIncident";
import UptimeBarDayModal from "Common/UI/Components/MonitorGraphs/UptimeBarDayModal";
import Color from "Common/Types/Color";
const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
@@ -110,6 +114,15 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
MonitorEvaluationSummary | undefined
>(undefined);
const [timelineIncidents, setTimelineIncidents] = useState<
Array<UptimeBarTooltipIncident>
>([]);
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [selectedDayIncidents, setSelectedDayIncidents] = useState<
Array<UptimeBarTooltipIncident>
>([]);
const getUptimePercent: () => ReactElement = (): ReactElement => {
if (isLoading) {
return <></>;
@@ -297,6 +310,66 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
);
setStatusTimelines(monitorStatus.data);
// Fetch incidents for this monitor in the timeline date range
const incidentResult: ListResult<Incident> = await ModelAPI.getList({
modelType: Incident,
query: {
monitors: [modelId] as any,
declaredAt: new InBetween(startDate, endDate),
projectId: ProjectUtil.getCurrentProjectId()!,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
_id: true,
title: true,
declaredAt: true,
incidentSeverity: {
name: true,
color: true,
},
currentIncidentState: {
_id: true,
name: true,
color: true,
},
monitors: {
_id: true,
},
},
sort: {
declaredAt: SortOrder.Descending,
},
});
const parsedIncidents: Array<UptimeBarTooltipIncident> =
incidentResult.data.map((incident: Incident) => {
return {
id: incident._id || "",
title: incident.title || "",
declaredAt: incident.declaredAt || new Date(),
incidentSeverity: incident.incidentSeverity
? {
name: incident.incidentSeverity.name || "",
color:
incident.incidentSeverity.color || new Color("#000000"),
}
: undefined,
currentIncidentState: incident.currentIncidentState
? {
name: incident.currentIncidentState.name || "",
color:
incident.currentIncidentState.color || new Color("#000000"),
}
: undefined,
monitorIds: (incident.monitors || []).map((m: Monitor) => {
return new ObjectID(m._id?.toString() || "");
}),
};
});
setTimelineIncidents(parsedIncidents);
const isMonitoredByProbe: boolean = item.monitorType
? MonitorTypeHelper.isProbableMonitor(item.monitorType)
: false;
@@ -614,9 +687,42 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
isLoading={isLoading}
defaultBarColor={Green}
downtimeMonitorStatuses={downTimeMonitorStatues}
incidents={timelineIncidents}
onIncidentClick={(incidentId: string) => {
Navigation.navigate(
RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW]!, {
modelId: new ObjectID(incidentId),
}),
);
}}
onBarClick={(
date: Date,
incidents: Array<UptimeBarTooltipIncident>,
) => {
setSelectedDay(date);
setSelectedDayIncidents(incidents);
}}
/>
</Card>
{selectedDay && (
<UptimeBarDayModal
date={selectedDay}
incidents={selectedDayIncidents}
onIncidentClick={(incidentId: string) => {
Navigation.navigate(
RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW]!, {
modelId: new ObjectID(incidentId),
}),
);
}}
onClose={() => {
setSelectedDay(null);
setSelectedDayIncidents([]);
}}
/>
)}
<Summary
monitorType={monitorType!}
probes={probes}

View File

@@ -1,21 +1,108 @@
import DisabledWarning from "../../../Components/Monitor/DisabledWarning";
import MonitorMetricsElement from "../../../Components/Monitor/MonitorMetrics";
import MonitorCustomMetrics from "../../../Components/Monitor/MonitorCustomMetrics";
import MonitorIncidentMetrics from "../../../Components/Monitor/MonitorIncidentMetrics";
import MonitorAlertMetrics from "../../../Components/Monitor/MonitorAlertMetrics";
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import MonitorMetricsElement from "../../../Components/Monitor/MonitorMetrics";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import MonitorType from "Common/Types/Monitor/MonitorType";
import MonitorMetricTypeUtil from "Common/Utils/Monitor/MonitorMetricType";
import Monitor from "Common/Models/DatabaseModels/Monitor";
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 ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
const MonitorDelete: FunctionComponent<
const MonitorMetrics: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [, setCurrentTab] = useState<Tab | null>(null);
const [monitorType, setMonitorType] = useState<MonitorType | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
setIsLoading(true);
ModelAPI.getItem({
modelType: Monitor,
id: modelId,
select: { monitorType: true },
})
.then((item: Monitor | null) => {
setMonitorType(item?.monitorType || null);
setIsLoading(false);
})
.catch((err: Error) => {
setError(API.getFriendlyMessage(err));
setIsLoading(false);
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
const hasMonitorMetrics: boolean =
monitorType !== null &&
MonitorMetricTypeUtil.getMonitorMetricTypesByMonitorType(monitorType)
.length > 0;
const tabs: Array<Tab> = [];
if (hasMonitorMetrics) {
tabs.push({
name: "Monitor Metrics",
children: <MonitorMetricsElement monitorId={modelId} />,
});
}
if (
monitorType === MonitorType.CustomJavaScriptCode ||
monitorType === MonitorType.SyntheticMonitor
) {
tabs.push({
name: "Custom Metrics",
children: <MonitorCustomMetrics monitorId={modelId} />,
});
}
tabs.push({
name: "Incident Metrics",
children: <MonitorIncidentMetrics monitorId={modelId} />,
});
tabs.push({
name: "Alert Metrics",
children: <MonitorAlertMetrics monitorId={modelId} />,
});
return (
<Fragment>
<DisabledWarning monitorId={modelId} />
<MonitorMetricsElement monitorId={modelId} />
<Tabs
tabs={tabs}
onTabChange={(tab: Tab) => {
setCurrentTab(tab);
}}
/>
</Fragment>
);
};
export default MonitorDelete;
export default MonitorMetrics;

View File

@@ -46,18 +46,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
},
];
if (MonitorTypeHelper.doesMonitorTypeHaveGraphs(props.monitorType)) {
overviewItems.push({
link: {
title: "Metrics",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MONITOR_VIEW_METRICS] as Route,
{ modelId: props.modelId },
),
},
icon: IconProp.Graph,
});
}
overviewItems.push({
link: {
title: "Metrics",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MONITOR_VIEW_METRICS] as Route,
{ modelId: props.modelId },
),
},
icon: IconProp.Graph,
});
overviewItems.push({
link: {

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

@@ -46,6 +46,7 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
const [nodes, setNodes] = useState<Array<Node>>([]);
const [edges, setEdges] = useState<Array<Edge>>([]);
const [error, setError] = useState<string>("");
const [webhookSecretKey, setWebhookSecretKey] = useState<string>("");
const [showRunSuccessConfirmation, setShowRunSuccessConfirmation] =
useState<boolean>(false);
@@ -63,11 +64,16 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
id: modelId,
select: {
graph: true,
webhookSecretKey: true,
},
requestOptions: {},
});
if (workflow) {
if (workflow.webhookSecretKey) {
setWebhookSecretKey(workflow.webhookSecretKey);
}
const allComponents: {
components: Array<ComponentMetadata>;
categories: Array<ComponentCategory>;
@@ -349,6 +355,7 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
) : (
<Workflow
workflowId={modelId}
webhookSecretKey={webhookSecretKey}
showComponentsPickerModal={showComponentPickerModal}
onComponentPickerModalUpdate={(value: boolean) => {
setShowComponentPickerModal(value);

View File

@@ -5,15 +5,161 @@ import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import DuplicateModel from "Common/UI/Components/DuplicateModel/DuplicateModel";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FieldType from "Common/UI/Components/Types/FieldType";
import Navigation from "Common/UI/Utils/Navigation";
import Workflow from "Common/Models/DatabaseModels/Workflow";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useState,
} from "react";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import UUID from "Common/Utils/UUID";
import ComponentID from "Common/Types/Workflow/ComponentID";
import { JSONObject } from "Common/Types/JSON";
import {
ComponentType,
NodeDataProp,
NodeType,
} from "Common/Types/Workflow/Component";
import { useAsyncEffect } from "use-async-effect";
const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [showResetConfirmation, setShowResetConfirmation] =
useState<boolean>(false);
const [refresher, setRefresher] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [isWebhookTrigger, setIsWebhookTrigger] = useState<boolean>(false);
useAsyncEffect(async () => {
try {
const workflow: Workflow | null = await ModelAPI.getItem({
modelType: Workflow,
id: modelId,
select: {
graph: true,
},
requestOptions: {},
});
if (workflow?.graph && (workflow.graph as JSONObject)["nodes"]) {
const nodes: Array<JSONObject> = (workflow.graph as JSONObject)[
"nodes"
] as Array<JSONObject>;
for (const node of nodes) {
const nodeData: NodeDataProp = node["data"] as any;
if (
nodeData.componentType === ComponentType.Trigger &&
nodeData.nodeType === NodeType.Node &&
nodeData.metadataId === ComponentID.Webhook
) {
setIsWebhookTrigger(true);
break;
}
}
}
} catch {
// ignore - just don't show the webhook section
}
}, []);
const resetSecretKey: () => void = (): void => {
setShowResetConfirmation(false);
ModelAPI.updateById({
modelType: Workflow,
id: modelId,
data: {
webhookSecretKey: UUID.generate(),
},
})
.then(() => {
setRefresher(!refresher);
})
.catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
};
return (
<Fragment>
{isWebhookTrigger && (
<CardModelDetail<Workflow>
name="Workflow > Webhook Secret Key"
cardProps={{
title: "Webhook Secret Key",
description:
"This secret key is used to trigger this workflow via webhook. Use this key in the webhook URL instead of the workflow ID for security. You can reset this key if it is compromised.",
buttons: [
{
title: "Reset Secret Key",
buttonStyle: ButtonStyleType.DANGER_OUTLINE,
onClick: () => {
setShowResetConfirmation(true);
},
icon: IconProp.Refresh,
},
],
}}
isEditable={false}
refresher={refresher}
modelDetailProps={{
showDetailsInNumberOfColumns: 1,
modelType: Workflow,
id: "model-detail-workflow-webhook-secret",
fields: [
{
field: {
webhookSecretKey: true,
},
fieldType: FieldType.HiddenText,
title: "Webhook Secret Key",
placeholder:
"No secret key generated yet. Save the workflow to generate one.",
opts: {
isCopyable: true,
},
},
],
modelId: modelId,
}}
/>
)}
{showResetConfirmation && (
<ConfirmModal
title="Reset Webhook Secret Key"
description="Are you sure you want to reset the webhook secret key? Any existing integrations using the current key will stop working."
submitButtonText="Reset Key"
submitButtonType={ButtonStyleType.DANGER}
onClose={() => {
setShowResetConfirmation(false);
}}
onSubmit={resetSecretKey}
/>
)}
{error && (
<ConfirmModal
title="Error"
description={error}
submitButtonText="Close"
submitButtonType={ButtonStyleType.NORMAL}
onSubmit={() => {
setError("");
}}
/>
)}
<DuplicateModel
modelId={modelId}
modelType={Workflow}

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

@@ -4,7 +4,7 @@ Custom Code Monitor allows you to write custom scripts to monitor your applicati
#### Example
The following example shows how to use a Synthetic Monitor:
The following example shows how to use a Custom Code Monitor:
```javascript
// You can use axios module.
@@ -50,10 +50,49 @@ console.log(stringSecret);
```
### Custom Metrics
You can capture custom metrics from your script using the `oneuptime.captureMetric()` function. These metrics are stored in OneUptime and can be charted on dashboards using the Metric Explorer.
```javascript
oneuptime.captureMetric(name, value, attributes);
```
- `name` (string, required): The metric name (e.g. `"api.response.time"`). It will be stored with a `custom.monitor.` prefix automatically.
- `value` (number, required): The numeric metric value.
- `attributes` (object, optional): Key-value pairs for additional context.
#### Example
```javascript
const response = await axios.get('https://api.example.com/health');
// Capture a simple metric
oneuptime.captureMetric('api.response.time', response.data.latency);
// Capture a metric with attributes
oneuptime.captureMetric('api.queue.depth', response.data.queueDepth, {
region: 'us-east-1',
environment: 'production'
});
return {
data: response.data
};
```
Once captured, these metrics appear in the Metric Explorer under names like `custom.monitor.api.response.time`. You can add them to dashboard charts, set up alerts, and filter by monitor, probe, or any custom attributes you provided.
**Limits:**
- Maximum 100 metrics per script execution.
- Metric names are limited to 200 characters.
- Values must be numeric.
### Modules available in the script
- `axios`: You can use this module to make HTTP requests. It is a promise-based HTTP client for the browser and Node.js.
- `crypto`: You can use this module to perform cryptographic operations. It is a built-in Node.js module that provides cryptographic functionality that includes a set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify functions.
- `console.log`: You can use this module to log data to the console. This is useful for debugging purposes.
- `oneuptime.captureMetric`: You can use this to capture custom metrics from your script. See the Custom Metrics section above.
- `http`: You can use this module to make HTTP requests. It is a built-in Node.js module that provides an HTTP client and server.
- `https`: You can use this module to make HTTPS requests. It is a built-in Node.js module that provides an HTTPS client and server.

View File

@@ -103,11 +103,54 @@ let booleanSecret = {{monitorSecrets.BooleanSecret}};
console.log(stringSecret);
```
### Custom Metrics
You can capture custom metrics from your script using the `oneuptime.captureMetric()` function. These metrics are stored in OneUptime and can be charted on dashboards using the Metric Explorer.
```javascript
oneuptime.captureMetric(name, value, attributes);
```
- `name` (string, required): The metric name (e.g. `"dashboard.load.time"`). It will be stored with a `custom.monitor.` prefix automatically.
- `value` (number, required): The numeric metric value.
- `attributes` (object, optional): Key-value pairs for additional context.
#### Example
```javascript
await page.goto('https://app.example.com');
const startTime = Date.now();
await page.waitForSelector('#dashboard-loaded');
const loadTime = Date.now() - startTime;
// Capture page load time as a custom metric
oneuptime.captureMetric('dashboard.load.time', loadTime, {
page: 'dashboard'
});
const screenshots = {};
screenshots['dashboard'] = await page.screenshot();
return {
data: { loadTime },
screenshots: screenshots
};
```
Once captured, these metrics appear in the Metric Explorer under names like `custom.monitor.dashboard.load.time`. You can add them to dashboard charts, set up alerts, and filter by monitor, probe, browser type, screen size, or any custom attributes you provided.
**Limits:**
- Maximum 100 metrics per script execution.
- Metric names are limited to 200 characters.
- Values must be numeric.
### Modules available in the script
- `page`: You can use this module to interact with the browser. It is a Playwright Page object that allows you to perform actions like clicking buttons, filling forms, and taking screenshots. You can access the browser context via `page.context()` if needed (for example, to create a new page or deal with popups).
- `axios`: You can use this module to make HTTP requests. It is a promise-based HTTP client for the browser and Node.js.
- `crypto`: You can use this module to perform cryptographic operations. It is a built-in Node.js module that provides cryptographic functionality that includes a set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify functions.
- `console.log`: You can use this module to log data to the console. This is useful for debugging purposes.
- `oneuptime.captureMetric`: You can use this to capture custom metrics from your script. See the Custom Metrics section above.
- `http`: You can use this module to make HTTP requests. It is a built-in Node.js module that provides an HTTP client and server.
- `https`: You can use this module to make HTTPS requests. It is a built-in Node.js module that provides an HTTPS client and server.

View File

@@ -72,7 +72,7 @@ pyroscope.ebpf "default" {
pyroscope.write "oneuptime" {
endpoint {
url = "https://oneuptime.com/otlp/v1/profiles"
url = "https://oneuptime.com/pyroscope"
headers = {
"x-oneuptime-token" = "YOUR_ONEUPTIME_SERVICE_TOKEN",
}

View File

@@ -81,6 +81,12 @@ export default class SSOUtil {
throw new BadRequestException("SAML Assertion not found");
}
if (samlAssertion.length !== 1) {
throw new BadRequestException(
"Expected exactly one Assertion in SAML Response",
);
}
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)["saml2:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["saml:Subject"] as JSONArray) ||
@@ -158,6 +164,10 @@ export default class SSOUtil {
return null;
}
if (samlAssertion.length !== 1) {
return null;
}
const samlAttributeStatement: JSONArray =
((samlAssertion[0] as JSONObject)[
"saml2:AttributeStatement"
@@ -242,6 +252,12 @@ export default class SSOUtil {
throw new BadRequestException("SAML Assertion not found");
}
if (samlAssertion.length !== 1) {
throw new BadRequestException(
"Expected exactly one Assertion in SAML Response",
);
}
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)["saml2:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["saml:Subject"] as JSONArray) ||

View File

@@ -2,6 +2,7 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpg";
declare module "*.gif";
declare module "*.css";
declare module "react-syntax-highlighter/dist/esm/prism-light";
declare module "react-syntax-highlighter/dist/esm/styles/prism";

View File

@@ -52,6 +52,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -84,7 +85,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -382,9 +383,9 @@
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -692,9 +693,9 @@
}
},
"node_modules/nodemon/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -52,6 +52,7 @@
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@pyroscope/nodejs": "^0.4.11",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -84,7 +85,7 @@
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
"isolated-vm": "^6.0.2",
"isolated-vm": "^6.1.2",
"json2csv": "^5.0.7",
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
@@ -777,9 +778,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -900,9 +901,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"

View File

@@ -5,13 +5,15 @@ import Icon from "Common/UI/Components/Icon/Icon";
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
import MonitorUptimeGraph from "Common/UI/Components/MonitorGraphs/Uptime";
import UptimeUtil from "Common/UI/Components/MonitorGraphs/UptimeUtil";
import UptimeBarDayModal from "Common/UI/Components/MonitorGraphs/UptimeBarDayModal";
import Tooltip from "Common/UI/Components/Tooltip/Tooltip";
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
import MonitorStatusTimelne from "Common/Models/DatabaseModels/MonitorStatusTimeline";
import StatusPageHistoryChartBarColorRule from "Common/Models/DatabaseModels/StatusPageHistoryChartBarColorRule";
import UptimePrecision from "Common/Types/StatusPage/UptimePrecision";
import React, { FunctionComponent, ReactElement } from "react";
import UptimeBarTooltipIncident from "Common/Types/Monitor/UptimeBarTooltipIncident";
import React, { FunctionComponent, ReactElement, useState } from "react";
export interface ComponentProps {
monitorName: string;
@@ -31,11 +33,18 @@ export interface ComponentProps {
downtimeMonitorStatuses: Array<MonitorStatus>;
defaultBarColor: Color;
uptimeHistoryDays?: number | undefined;
incidents?: Array<UptimeBarTooltipIncident> | undefined;
onIncidentClick?: ((incidentId: string) => void) | undefined;
}
const MonitorOverview: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [selectedDayIncidents, setSelectedDayIncidents] = useState<
Array<UptimeBarTooltipIncident>
>([]);
const getCurrentStatus: GetReactElementFunction = (): ReactElement => {
// if the current status is operational then show uptime Percent.
@@ -137,6 +146,15 @@ const MonitorOverview: FunctionComponent<ComponentProps> = (
endDate={props.endDate}
isLoading={false}
height={props.uptimeGraphHeight}
incidents={props.incidents}
onIncidentClick={props.onIncidentClick}
onBarClick={(
date: Date,
incidents: Array<UptimeBarTooltipIncident>,
) => {
setSelectedDay(date);
setSelectedDayIncidents(incidents);
}}
/>
</div>
)}
@@ -148,6 +166,19 @@ const MonitorOverview: FunctionComponent<ComponentProps> = (
<div>Today</div>
</div>
)}
{/* Incident detail modal */}
{selectedDay && (
<UptimeBarDayModal
date={selectedDay}
incidents={selectedDayIncidents}
onIncidentClick={props.onIncidentClick}
onClose={() => {
setSelectedDay(null);
setSelectedDayIncidents([]);
}}
/>
)}
</div>
);
};

View File

@@ -6,6 +6,8 @@ import ScheduledMaintenanceGroup from "../../Types/ScheduledMaintenanceGroup";
import API from "../../Utils/API";
import { STATUS_PAGE_API_URL } from "../../Utils/Config";
import StatusPageUtil from "../../Utils/StatusPage";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import PageMap from "../../Utils/PageMap";
import { getAnnouncementEventItem } from "../Announcement/Detail";
import { getIncidentEventItem, getEpisodeEventItem } from "../Incidents/Detail";
import PageComponentProps from "../PageComponentProps";
@@ -40,6 +42,7 @@ import IncidentEpisodePublicNote from "Common/Models/DatabaseModels/IncidentEpis
import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline";
import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote";
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
import Monitor from "Common/Models/DatabaseModels/Monitor";
import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
import MonitorStatusTimeline from "Common/Models/DatabaseModels/MonitorStatusTimeline";
import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance";
@@ -59,6 +62,8 @@ import React, {
import UptimePrecision from "Common/Types/StatusPage/UptimePrecision";
import StatusPageResourceUptimeUtil from "Common/Utils/StatusPage/ResourceUptime";
import BadDataException from "Common/Types/Exception/BadDataException";
import UptimeBarTooltipIncident from "Common/Types/Monitor/UptimeBarTooltipIncident";
import Color from "Common/Types/Color";
const Overview: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -141,6 +146,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
const [monitorGroupCurrentStatuses, setMonitorGroupCurrentStatuses] =
useState<Dictionary<ObjectID>>({});
const [timelineIncidents, setTimelineIncidents] = useState<
Array<UptimeBarTooltipIncident>
>([]);
StatusPageUtil.checkIfUserHasLoggedIn();
const loadPage: PromiseVoidFunction = async (): Promise<void> => {
@@ -277,6 +286,39 @@ const Overview: FunctionComponent<PageComponentProps> = (
(data["monitorGroupCurrentStatuses"] as JSONObject) || {},
) as Dictionary<ObjectID>;
// Parse timeline incidents for uptime bar tooltips
const rawTimelineIncidents: Array<Incident> = BaseModel.fromJSONArray(
(data["timelineIncidents"] as JSONArray) || [],
Incident,
);
const parsedTimelineIncidents: Array<UptimeBarTooltipIncident> =
rawTimelineIncidents.map((incident: Incident) => {
return {
id: incident._id || "",
title: incident.title || "",
declaredAt: incident.declaredAt || new Date(),
incidentSeverity: incident.incidentSeverity
? {
name: incident.incidentSeverity.name || "",
color:
incident.incidentSeverity.color || new Color("#000000"),
}
: undefined,
currentIncidentState: incident.currentIncidentState
? {
name: incident.currentIncidentState.name || "",
color:
incident.currentIncidentState.color || new Color("#000000"),
}
: undefined,
monitorIds: (incident.monitors || []).map((m: Monitor) => {
return new ObjectID(m._id?.toString() || "");
}),
};
});
setTimelineIncidents(parsedTimelineIncidents);
setMonitorsInGroup(monitorsInGroup);
setMonitorGroupCurrentStatuses(monitorGroupCurrentStatuses);
@@ -463,6 +505,15 @@ const Overview: FunctionComponent<PageComponentProps> = (
currentStatus.color = Green;
}
const monitorId: string = resource.monitor?._id?.toString() || "";
const monitorIncidents: Array<UptimeBarTooltipIncident> =
timelineIncidents.filter((incident: UptimeBarTooltipIncident) => {
return incident.monitorIds.some((id: ObjectID) => {
return id.toString() === monitorId;
});
});
elements.push(
<MonitorOverview
key={Math.random()}
@@ -495,6 +546,17 @@ const Overview: FunctionComponent<PageComponentProps> = (
uptimeGraphHeight={10}
defaultBarColor={statusPage?.defaultBarColor || Green}
uptimeHistoryDays={uptimeHistoryDays}
incidents={monitorIncidents}
onIncidentClick={(incidentId: string) => {
Navigation.navigate(
RouteUtil.populateRouteParams(
StatusPageUtil.isPreviewPage()
? (RouteMap[PageMap.PREVIEW_INCIDENT_DETAIL] as Route)
: (RouteMap[PageMap.INCIDENT_DETAIL] as Route),
new ObjectID(incidentId),
),
);
}}
/>,
);
}
@@ -519,6 +581,20 @@ const Overview: FunctionComponent<PageComponentProps> = (
currentStatus.color = Green;
}
// Get monitor IDs in this group
const groupMonitorIds: Array<string> = (
monitorsInGroup[resource.monitorGroupId?.toString() || ""] || []
).map((id: ObjectID) => {
return id.toString();
});
const groupIncidents: Array<UptimeBarTooltipIncident> =
timelineIncidents.filter((incident: UptimeBarTooltipIncident) => {
return incident.monitorIds.some((id: ObjectID) => {
return groupMonitorIds.includes(id.toString());
});
});
elements.push(
<MonitorOverview
key={Math.random()}
@@ -551,6 +627,17 @@ const Overview: FunctionComponent<PageComponentProps> = (
uptimeGraphHeight={10}
defaultBarColor={statusPage?.defaultBarColor || Green}
uptimeHistoryDays={uptimeHistoryDays}
incidents={groupIncidents}
onIncidentClick={(incidentId: string) => {
Navigation.navigate(
RouteUtil.populateRouteParams(
StatusPageUtil.isPreviewPage()
? (RouteMap[PageMap.PREVIEW_INCIDENT_DETAIL] as Route)
: (RouteMap[PageMap.INCIDENT_DETAIL] as Route),
new ObjectID(incidentId),
),
);
}}
/>,
);
}

View File

@@ -25,6 +25,7 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/otlp/v1/traces",
OpenTelemetryRequestMiddleware.parseBody,
OpenTelemetryRequestMiddleware.getProductType,
TelemetryIngest.isAuthorizedServiceMiddleware,
async (
@@ -38,6 +39,7 @@ router.post(
router.post(
"/otlp/v1/metrics",
OpenTelemetryRequestMiddleware.parseBody,
OpenTelemetryRequestMiddleware.getProductType,
TelemetryIngest.isAuthorizedServiceMiddleware,
async (
@@ -51,6 +53,7 @@ router.post(
router.post(
"/otlp/v1/logs",
OpenTelemetryRequestMiddleware.parseBody,
OpenTelemetryRequestMiddleware.getProductType,
TelemetryIngest.isAuthorizedServiceMiddleware,
async (
@@ -64,6 +67,7 @@ router.post(
router.post(
"/otlp/v1/profiles",
OpenTelemetryRequestMiddleware.parseBody,
OpenTelemetryRequestMiddleware.getProductType,
TelemetryIngest.isAuthorizedServiceMiddleware,
async (

View File

@@ -0,0 +1,62 @@
import TelemetryIngest, {
TelemetryRequest,
} from "Common/Server/Middleware/TelemetryIngest";
import ProductType from "Common/Types/MeteredPlan/ProductType";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
RequestHandler,
} from "Common/Server/Utils/Express";
import PyroscopeIngestService from "../Services/PyroscopeIngestService";
import MultipartFormDataMiddleware from "Common/Server/Middleware/MultipartFormData";
const router: ExpressRouter = Express.getRouter();
// Set product type to Profiles for metering
const setProfilesProductType: RequestHandler = (
req: ExpressRequest,
_res: ExpressResponse,
next: NextFunction,
): void => {
(req as TelemetryRequest).productType = ProductType.Profiles;
next();
};
/*
* Map Authorization: Bearer <token> to x-oneuptime-token header
* Pyroscope SDKs use authToken which sends Authorization: Bearer
*/
const mapBearerTokenMiddleware: RequestHandler = (
req: ExpressRequest,
_res: ExpressResponse,
next: NextFunction,
): void => {
if (!req.headers["x-oneuptime-token"]) {
const authHeader: string | undefined = req.headers[
"authorization"
] as string;
if (authHeader && authHeader.startsWith("Bearer ")) {
req.headers["x-oneuptime-token"] = authHeader.substring(7);
}
}
next();
};
router.post(
"/pyroscope/ingest",
MultipartFormDataMiddleware,
mapBearerTokenMiddleware,
setProfilesProductType,
TelemetryIngest.isAuthorizedServiceMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
return PyroscopeIngestService.ingestPyroscopeProfile(req, res, next);
},
);
export default router;

View File

@@ -0,0 +1,80 @@
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", "/"];
const PROBE_INGEST_PREFIXES: Array<string> = [
"/probe-ingest",
"/ingestor",
"/",
];
const SERVER_MONITOR_PREFIXES: Array<string> = ["/server-monitor-ingest", "/"];
const INCOMING_REQUEST_PREFIXES: Array<string> = [
"/incoming-request-ingest",
"/",
];
const TelemetryFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
try {
/*
* Mount telemetry routes only during feature-set init so they sit behind
* the shared middleware stack from StartServer (body parsers, headers, etc.).
*/
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.
*/
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", "/"]
app.use(SERVER_MONITOR_PREFIXES, ServerMonitorAPI);
// IncomingRequestIngest routes under ["/incoming-request-ingest", "/"]
app.use(INCOMING_REQUEST_PREFIXES, IncomingRequestAPI);
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

@@ -5,28 +5,40 @@ import {
ExpressRequest,
ExpressResponse,
NextFunction,
headerValueToString,
} from "Common/Server/Utils/Express";
import CaptureSpan from "Common/Server/Utils/Telemetry/CaptureSpan";
import protobuf from "protobufjs";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import zlib from "zlib";
import { promisify } from "util";
// 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
@@ -34,8 +46,56 @@ const LogsData: protobuf.Type = LogsProto.lookupType("LogsData");
const TracesData: protobuf.Type = TracesProto.lookupType("TracesData");
const MetricsData: protobuf.Type = MetricsProto.lookupType("MetricsData");
const ProfilesData: protobuf.Type = ProfilesProto.lookupType("ProfilesData");
const gunzipAsync: (buffer: Uint8Array) => Promise<Buffer> = promisify(
zlib.gunzip,
);
export default class OpenTelemetryRequestMiddleware {
@CaptureSpan()
public static async parseBody(
req: ExpressRequest,
_res: ExpressResponse,
next: NextFunction,
): Promise<void> {
try {
if (req.body !== undefined && req.body !== null) {
return next();
}
const requestBuffer: Buffer = await new Promise<Buffer>(
(resolve: (value: Buffer) => void, reject: (err: Error) => void) => {
const chunks: Array<Buffer> = [];
req.on("data", (chunk: Buffer | string) => {
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8"),
);
});
req.on("end", () => {
resolve(Buffer.concat(chunks));
});
req.on("error", (err: Error) => {
reject(err);
});
},
);
const contentEncoding: string | undefined = headerValueToString(
req.headers["content-encoding"],
);
req.body = contentEncoding?.includes("gzip")
? await gunzipAsync(requestBuffer)
: requestBuffer;
next();
} catch (err) {
return next(err);
}
}
@CaptureSpan()
public static async getProductType(
req: ExpressRequest,
@@ -45,7 +105,9 @@ export default class OpenTelemetryRequestMiddleware {
try {
let productType: ProductType;
const contentType: string | undefined = req.headers["content-type"];
const contentType: string | undefined = headerValueToString(
req.headers["content-type"],
);
const isProtobuf: boolean =
req.body instanceof Uint8Array &&
(!contentType ||

Some files were not shown because too many files have changed in this diff Show More