@oneuptime/common 10.0.43 → 10.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/Models/DatabaseModels/Workflow.ts +29 -0
  2. package/Server/API/StatusPageAPI.ts +48 -0
  3. package/Server/EnvironmentConfig.ts +5 -8
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.ts +22 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  6. package/Server/Services/AlertService.ts +45 -0
  7. package/Server/Services/IncidentService.ts +81 -13
  8. package/Server/Services/LogAggregationService.ts +1 -0
  9. package/Server/Services/WorkflowService.ts +28 -1
  10. package/Server/Types/Workflow/Components/Webhook.ts +28 -5
  11. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +29 -13
  12. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +163 -26
  13. package/Server/Utils/Monitor/MonitorMetricUtil.ts +92 -0
  14. package/Server/Utils/Profiling.ts +101 -0
  15. package/Server/Utils/VM/VMRunner.ts +88 -0
  16. package/Types/Dashboard/DashboardTemplates.ts +1149 -0
  17. package/Types/Exception/ExceptionMetricType.ts +15 -0
  18. package/Types/IsolatedVM/ReturnResult.ts +3 -0
  19. package/Types/Metrics/MetricDashboardMetricType.ts +28 -0
  20. package/Types/Metrics/MetricsQuery.ts +2 -1
  21. package/Types/Monitor/CustomCodeMonitor/CapturedMetric.ts +7 -0
  22. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +2 -0
  23. package/Types/Monitor/UptimeBarTooltipIncident.ts +21 -0
  24. package/Types/Profile/ProfileMetricType.ts +16 -0
  25. package/Types/Span/SpanMetricType.ts +17 -0
  26. package/UI/Components/Charts/Area/AreaChart.tsx +40 -33
  27. package/UI/Components/Charts/Bar/BarChart.tsx +37 -30
  28. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +196 -51
  29. package/UI/Components/Charts/ChartGroup/NoDataMessage.tsx +13 -0
  30. package/UI/Components/Charts/Line/LineChart.tsx +39 -32
  31. package/UI/Components/Forms/BasicForm.tsx +1 -1
  32. package/UI/Components/Graphs/DayUptimeGraph.tsx +88 -35
  33. package/UI/Components/Graphs/UptimeBarTooltip.tsx +547 -0
  34. package/UI/Components/MonitorGraphs/Uptime.tsx +7 -0
  35. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +225 -0
  36. package/UI/Components/RadioButtons/GroupRadioButtons.tsx +1 -0
  37. package/UI/Components/Tooltip/Tooltip.tsx +29 -4
  38. package/UI/Components/Workflow/ComponentSettingsModal.tsx +2 -0
  39. package/UI/Components/Workflow/DocumentationViewer.tsx +5 -0
  40. package/UI/Components/Workflow/Workflow.tsx +2 -0
  41. package/Utils/Alerts/AlertMetricType.ts +98 -0
  42. package/Utils/Incident/IncidentMetricType.ts +130 -0
  43. package/build/dist/Models/DatabaseModels/Workflow.js +30 -0
  44. package/build/dist/Models/DatabaseModels/Workflow.js.map +1 -1
  45. package/build/dist/Server/API/StatusPageAPI.js +42 -0
  46. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  47. package/build/dist/Server/EnvironmentConfig.js +2 -2
  48. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.js +14 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.js.map +1 -0
  51. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  52. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  53. package/build/dist/Server/Services/AlertService.js +34 -0
  54. package/build/dist/Server/Services/AlertService.js.map +1 -1
  55. package/build/dist/Server/Services/IncidentService.js +52 -9
  56. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  57. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  58. package/build/dist/Server/Services/WorkflowService.js +25 -0
  59. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  60. package/build/dist/Server/Types/Workflow/Components/Webhook.js +23 -5
  61. package/build/dist/Server/Types/Workflow/Components/Webhook.js.map +1 -1
  62. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +21 -7
  63. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  64. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +120 -21
  65. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  66. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +68 -1
  67. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  68. package/build/dist/Server/Utils/Profiling.js +80 -0
  69. package/build/dist/Server/Utils/Profiling.js.map +1 -0
  70. package/build/dist/Server/Utils/VM/VMRunner.js +68 -0
  71. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  72. package/build/dist/Types/Dashboard/DashboardTemplates.js +1095 -0
  73. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  74. package/build/dist/Types/Exception/ExceptionMetricType.js +16 -0
  75. package/build/dist/Types/Exception/ExceptionMetricType.js.map +1 -0
  76. package/build/dist/Types/Metrics/MetricDashboardMetricType.js +26 -0
  77. package/build/dist/Types/Metrics/MetricDashboardMetricType.js.map +1 -0
  78. package/build/dist/Types/Monitor/CustomCodeMonitor/CapturedMetric.js +2 -0
  79. package/build/dist/Types/Monitor/CustomCodeMonitor/CapturedMetric.js.map +1 -0
  80. package/build/dist/Types/Monitor/UptimeBarTooltipIncident.js +2 -0
  81. package/build/dist/Types/Monitor/UptimeBarTooltipIncident.js.map +1 -0
  82. package/build/dist/Types/Profile/ProfileMetricType.js +17 -0
  83. package/build/dist/Types/Profile/ProfileMetricType.js.map +1 -0
  84. package/build/dist/Types/Span/SpanMetricType.js +18 -0
  85. package/build/dist/Types/Span/SpanMetricType.js.map +1 -0
  86. package/build/dist/UI/Components/Charts/Area/AreaChart.js +21 -16
  87. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  88. package/build/dist/UI/Components/Charts/Bar/BarChart.js +20 -15
  89. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  90. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +73 -15
  91. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  92. package/build/dist/UI/Components/Charts/ChartGroup/NoDataMessage.js +7 -0
  93. package/build/dist/UI/Components/Charts/ChartGroup/NoDataMessage.js.map +1 -0
  94. package/build/dist/UI/Components/Charts/Line/LineChart.js +20 -15
  95. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  96. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  97. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  98. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +46 -20
  99. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  100. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +303 -0
  101. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -0
  102. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
  103. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  104. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +118 -0
  105. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -0
  106. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
  107. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
  108. package/build/dist/UI/Components/Tooltip/Tooltip.js +13 -3
  109. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  110. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +1 -1
  111. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  112. package/build/dist/UI/Components/Workflow/DocumentationViewer.js +1 -0
  113. package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
  114. package/build/dist/UI/Components/Workflow/Workflow.js +1 -1
  115. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  116. package/build/dist/Utils/Alerts/AlertMetricType.js +84 -0
  117. package/build/dist/Utils/Alerts/AlertMetricType.js.map +1 -0
  118. package/build/dist/Utils/Incident/IncidentMetricType.js +114 -0
  119. package/build/dist/Utils/Incident/IncidentMetricType.js.map +1 -0
  120. package/package.json +3 -2
@@ -1,4 +1,5 @@
1
1
  import logger from "../Logger";
2
+ import LogAggregationService from "../../Services/LogAggregationService";
2
3
  import VMUtil from "../VM/VMAPI";
3
4
  import APIRequestCriteria from "./Criteria/APIRequestCriteria";
4
5
  import CustomCodeMonitoringCriteria from "./Criteria/CustomCodeMonitorCriteria";
@@ -116,7 +117,7 @@ export default class MonitorCriteriaEvaluator {
116
117
  `;
117
118
 
118
119
  const contextBlock: string | null =
119
- MonitorCriteriaEvaluator.buildRootCauseContext({
120
+ await MonitorCriteriaEvaluator.buildRootCauseContext({
120
121
  dataToProcess: input.dataToProcess,
121
122
  monitorStep: input.monitorStep,
122
123
  monitor: input.monitor,
@@ -557,14 +558,16 @@ ${contextBlock}
557
558
  return null;
558
559
  }
559
560
 
560
- private static buildRootCauseContext(input: {
561
+ private static async buildRootCauseContext(input: {
561
562
  dataToProcess: DataToProcess;
562
563
  monitorStep: MonitorStep;
563
564
  monitor: Monitor;
564
- }): string | null {
565
+ }): Promise<string | null> {
565
566
  // Handle Kubernetes monitors with rich resource context
566
567
  if (input.monitor.monitorType === MonitorType.Kubernetes) {
567
- return MonitorCriteriaEvaluator.buildKubernetesRootCauseContext(input);
568
+ return await MonitorCriteriaEvaluator.buildKubernetesRootCauseContext(
569
+ input,
570
+ );
568
571
  }
569
572
 
570
573
  const requestDetails: Array<string> = [];
@@ -675,11 +678,11 @@ ${contextBlock}
675
678
  return sections.join("\n");
676
679
  }
677
680
 
678
- private static buildKubernetesRootCauseContext(input: {
681
+ private static async buildKubernetesRootCauseContext(input: {
679
682
  dataToProcess: DataToProcess;
680
683
  monitorStep: MonitorStep;
681
684
  monitor: Monitor;
682
- }): string | null {
685
+ }): Promise<string | null> {
683
686
  const metricResponse: MetricMonitorResponse =
684
687
  input.dataToProcess as MetricMonitorResponse;
685
688
 
@@ -713,51 +716,121 @@ ${contextBlock}
713
716
  if (breakdown.affectedResources && breakdown.affectedResources.length > 0) {
714
717
  const resourceLines: Array<string> = [];
715
718
 
716
- // Sort by metric value descending (worst first)
719
+ // Sort by metric value descending (worst first) and filter out zero-value resources
717
720
  const sortedResources: Array<KubernetesAffectedResource> = [
718
721
  ...breakdown.affectedResources,
719
- ].sort((a: KubernetesAffectedResource, b: KubernetesAffectedResource) => {
720
- return b.metricValue - a.metricValue;
721
- });
722
+ ]
723
+ .filter((r: KubernetesAffectedResource) => {
724
+ return r.metricValue > 0;
725
+ })
726
+ .sort(
727
+ (a: KubernetesAffectedResource, b: KubernetesAffectedResource) => {
728
+ return b.metricValue - a.metricValue;
729
+ },
730
+ );
731
+
732
+ if (sortedResources.length === 0) {
733
+ return sections.join("\n");
734
+ }
722
735
 
723
736
  // Show top 10 affected resources
724
737
  const resourcesToShow: Array<KubernetesAffectedResource> =
725
738
  sortedResources.slice(0, 10);
726
739
 
740
+ // Determine which columns are present across all resources
741
+ const hasNamespace: boolean = resourcesToShow.some(
742
+ (r: KubernetesAffectedResource) => {
743
+ return r.namespace;
744
+ },
745
+ );
746
+ const hasWorkload: boolean = resourcesToShow.some(
747
+ (r: KubernetesAffectedResource) => {
748
+ return r.workloadType && r.workloadName;
749
+ },
750
+ );
751
+ const hasPod: boolean = resourcesToShow.some(
752
+ (r: KubernetesAffectedResource) => {
753
+ return r.podName;
754
+ },
755
+ );
756
+ const hasContainer: boolean = resourcesToShow.some(
757
+ (r: KubernetesAffectedResource) => {
758
+ return r.containerName;
759
+ },
760
+ );
761
+ const hasNode: boolean = resourcesToShow.some(
762
+ (r: KubernetesAffectedResource) => {
763
+ return r.nodeName;
764
+ },
765
+ );
766
+
767
+ // Build table header
768
+ const headerCells: Array<string> = [];
769
+ if (hasNamespace) {
770
+ headerCells.push("Namespace");
771
+ }
772
+ if (hasWorkload) {
773
+ headerCells.push("Workload Type");
774
+ headerCells.push("Workload");
775
+ }
776
+ if (hasPod) {
777
+ headerCells.push("Pod");
778
+ }
779
+ if (hasContainer) {
780
+ headerCells.push("Container");
781
+ }
782
+ if (hasNode) {
783
+ headerCells.push("Node");
784
+ }
785
+ headerCells.push("Value");
786
+
787
+ const headerRow: string = `| ${headerCells.join(" | ")} |`;
788
+ const separatorRow: string = `| ${headerCells
789
+ .map(() => {
790
+ return "---";
791
+ })
792
+ .join(" | ")} |`;
793
+
794
+ resourceLines.push(headerRow);
795
+ resourceLines.push(separatorRow);
796
+
727
797
  for (const resource of resourcesToShow) {
728
- const details: Array<string> = [];
798
+ const cells: Array<string> = [];
729
799
 
730
- if (resource.namespace) {
731
- details.push(`Namespace: \`${resource.namespace}\``);
800
+ if (hasNamespace) {
801
+ cells.push(resource.namespace ? `\`${resource.namespace}\`` : "-");
732
802
  }
733
- if (resource.workloadType && resource.workloadName) {
734
- details.push(
735
- `${resource.workloadType}: \`${resource.workloadName}\``,
803
+ if (hasWorkload) {
804
+ cells.push(resource.workloadType ? `${resource.workloadType}` : "-");
805
+ cells.push(
806
+ resource.workloadName ? `\`${resource.workloadName}\`` : "-",
736
807
  );
737
808
  }
738
- if (resource.podName) {
739
- details.push(`Pod: \`${resource.podName}\``);
809
+ if (hasPod) {
810
+ cells.push(resource.podName ? `\`${resource.podName}\`` : "-");
740
811
  }
741
- if (resource.containerName) {
742
- details.push(`Container: \`${resource.containerName}\``);
812
+ if (hasContainer) {
813
+ cells.push(
814
+ resource.containerName ? `\`${resource.containerName}\`` : "-",
815
+ );
743
816
  }
744
- if (resource.nodeName) {
745
- details.push(`Node: \`${resource.nodeName}\``);
817
+ if (hasNode) {
818
+ cells.push(resource.nodeName ? `\`${resource.nodeName}\`` : "-");
746
819
  }
747
820
 
748
- details.push(`Value: **${resource.metricValue}**`);
821
+ cells.push(`**${resource.metricValue}**`);
749
822
 
750
- resourceLines.push(`- ${details.join(" | ")}`);
823
+ resourceLines.push(`| ${cells.join(" | ")} |`);
751
824
  }
752
825
 
753
826
  if (sortedResources.length > 10) {
754
827
  resourceLines.push(
755
- `- ... and ${sortedResources.length - 10} more affected resources`,
828
+ `\n*... and ${sortedResources.length - 10} more affected resources*`,
756
829
  );
757
830
  }
758
831
 
759
832
  sections.push(
760
- `\n\n**Affected Resources** (${sortedResources.length} total)\n${resourceLines.join("\n")}`,
833
+ `\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
761
834
  );
762
835
 
763
836
  // Add root cause analysis based on metric type
@@ -770,6 +843,70 @@ ${contextBlock}
770
843
  if (analysis) {
771
844
  sections.push(`\n\n**Root Cause Analysis**\n${analysis}`);
772
845
  }
846
+
847
+ // Fetch recent container logs for the top affected resource during CrashLoopBackOff
848
+ if (
849
+ (breakdown.metricName === "k8s.container.restarts" ||
850
+ breakdown.metricName.includes("restart")) &&
851
+ input.monitor.projectId
852
+ ) {
853
+ const topResource: KubernetesAffectedResource = resourcesToShow[0]!;
854
+
855
+ try {
856
+ const logAttributes: Record<string, string> = {};
857
+
858
+ if (breakdown.clusterName) {
859
+ logAttributes["resource.k8s.cluster.name"] = breakdown.clusterName;
860
+ }
861
+
862
+ if (topResource.podName) {
863
+ logAttributes["resource.k8s.pod.name"] = topResource.podName;
864
+ }
865
+
866
+ if (topResource.containerName) {
867
+ logAttributes["resource.k8s.container.name"] =
868
+ topResource.containerName;
869
+ }
870
+
871
+ if (topResource.namespace) {
872
+ logAttributes["resource.k8s.namespace.name"] =
873
+ topResource.namespace;
874
+ }
875
+
876
+ const now: Date = OneUptimeDate.getCurrentDate();
877
+ const fifteenMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
878
+ now,
879
+ -15,
880
+ );
881
+
882
+ const logs: Array<JSONObject> =
883
+ await LogAggregationService.getExportLogs({
884
+ projectId: input.monitor.projectId,
885
+ startTime: fifteenMinutesAgo,
886
+ endTime: now,
887
+ limit: 50,
888
+ attributes: logAttributes,
889
+ });
890
+
891
+ if (logs.length > 0) {
892
+ const logLines: Array<string> = logs.map((log: JSONObject) => {
893
+ const timestamp: string = log["time"] ? String(log["time"]) : "";
894
+ const severity: string = log["severityText"]
895
+ ? String(log["severityText"])
896
+ : "INFO";
897
+ const body: string = log["body"] ? String(log["body"]) : "";
898
+ return `\`${timestamp}\` **${severity}** ${body}`;
899
+ });
900
+
901
+ sections.push(
902
+ `\n\n**Recent Container Logs** (${topResource.podName || "unknown pod"} / ${topResource.containerName || "unknown container"}, last 15 minutes)\n\n${logLines.join("\n\n")}`,
903
+ );
904
+ }
905
+ } catch (err) {
906
+ logger.error("Failed to fetch container logs for root cause context");
907
+ logger.error(err);
908
+ }
909
+ }
773
910
  }
774
911
 
775
912
  return sections.join("\n");
@@ -13,6 +13,7 @@ import MetricType from "../../../Models/DatabaseModels/MetricType";
13
13
  import BasicInfrastructureMetrics from "../../../Types/Infrastructure/BasicMetrics";
14
14
  import Dictionary from "../../../Types/Dictionary";
15
15
  import { JSONObject } from "../../../Types/JSON";
16
+ import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
16
17
  import MonitorMetricType from "../../../Types/Monitor/MonitorMetricType";
17
18
  import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
18
19
  import ServerMonitorResponse from "../../../Types/Monitor/ServerMonitor/ServerMonitorResponse";
@@ -533,6 +534,97 @@ export default class MonitorMetricUtil {
533
534
  metricType;
534
535
  }
535
536
 
537
+ // Process custom metrics from Custom Code and Synthetic Monitor responses
538
+ const customCodeMetrics: CapturedMetric[] =
539
+ (data.dataToProcess as ProbeMonitorResponse).customCodeMonitorResponse
540
+ ?.capturedMetrics || [];
541
+
542
+ const syntheticCustomMetrics: CapturedMetric[] = [];
543
+ const syntheticResponsesForMetrics: Array<SyntheticMonitorResponse> =
544
+ (data.dataToProcess as ProbeMonitorResponse).syntheticMonitorResponse ||
545
+ [];
546
+ for (const resp of syntheticResponsesForMetrics) {
547
+ if (resp.capturedMetrics) {
548
+ syntheticCustomMetrics.push(...resp.capturedMetrics);
549
+ }
550
+ }
551
+
552
+ const allCustomMetrics: CapturedMetric[] = [
553
+ ...customCodeMetrics,
554
+ ...syntheticCustomMetrics,
555
+ ].slice(0, 100);
556
+
557
+ if (allCustomMetrics.length > 0) {
558
+ logger.debug(
559
+ `${data.monitorId.toString()} - Processing ${allCustomMetrics.length} custom metrics`,
560
+ );
561
+ }
562
+
563
+ const reservedAttributeKeys: Set<string> = new Set([
564
+ "monitorId",
565
+ "projectId",
566
+ "monitorName",
567
+ "probeName",
568
+ "probeId",
569
+ ]);
570
+
571
+ for (const customMetric of allCustomMetrics) {
572
+ if (
573
+ !customMetric.name ||
574
+ typeof customMetric.name !== "string" ||
575
+ typeof customMetric.value !== "number" ||
576
+ isNaN(customMetric.value)
577
+ ) {
578
+ continue;
579
+ }
580
+
581
+ const prefixedName: string = `custom.monitor.${customMetric.name}`;
582
+
583
+ const extraAttributes: JSONObject = {
584
+ isCustomMetric: "true",
585
+ };
586
+
587
+ if ((data.dataToProcess as ProbeMonitorResponse).probeId) {
588
+ extraAttributes["probeId"] = (
589
+ data.dataToProcess as ProbeMonitorResponse
590
+ ).probeId.toString();
591
+ }
592
+
593
+ if (customMetric.attributes) {
594
+ for (const [key, val] of Object.entries(customMetric.attributes)) {
595
+ if (typeof val === "string" && !reservedAttributeKeys.has(key)) {
596
+ extraAttributes[key] = val;
597
+ }
598
+ }
599
+ }
600
+
601
+ const attributes: JSONObject = this.buildMonitorMetricAttributes({
602
+ monitorId: data.monitorId,
603
+ projectId: data.projectId,
604
+ monitorName: data.monitorName,
605
+ probeName: data.probeName,
606
+ extraAttributes: extraAttributes,
607
+ });
608
+
609
+ const metricRow: JSONObject = await this.buildMonitorMetricRow({
610
+ projectId: data.projectId,
611
+ monitorId: data.monitorId,
612
+ metricName: prefixedName,
613
+ value: customMetric.value,
614
+ attributes: attributes,
615
+ metricPointType: MetricPointType.Gauge,
616
+ });
617
+
618
+ metricRows.push(metricRow);
619
+
620
+ const metricType: MetricType = new MetricType();
621
+ metricType.name = prefixedName;
622
+ metricType.description = `Custom metric: ${customMetric.name}`;
623
+ metricType.unit = "";
624
+
625
+ metricNameServiceNameMap[prefixedName] = metricType;
626
+ }
627
+
536
628
  if (metricRows.length > 0) {
537
629
  await MetricService.insertJsonRows(metricRows);
538
630
  }
@@ -0,0 +1,101 @@
1
+ import Pyroscope from "@pyroscope/nodejs";
2
+ import { EnableProfiling } from "../EnvironmentConfig";
3
+ import logger from "./Logger";
4
+
5
+ export default class Profiling {
6
+ public static init(data: { serviceName: string }): void {
7
+ if (!EnableProfiling) {
8
+ return;
9
+ }
10
+
11
+ const serverAddress: string | undefined = this.getServerAddress();
12
+ const authToken: string | undefined = this.getAuthToken();
13
+
14
+ if (!serverAddress) {
15
+ logger.warn(
16
+ "Profiling enabled but OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT not configured. Skipping profiling initialization.",
17
+ );
18
+ return;
19
+ }
20
+
21
+ try {
22
+ Pyroscope.init({
23
+ appName: data.serviceName,
24
+ serverAddress: serverAddress,
25
+ authToken: authToken,
26
+ wall: {
27
+ collectCpuTime: true,
28
+ },
29
+ });
30
+
31
+ Pyroscope.start();
32
+
33
+ logger.info(
34
+ `Profiling initialized for service: ${data.serviceName} -> ${serverAddress}`,
35
+ );
36
+ } catch (err) {
37
+ logger.error("Failed to initialize profiling:");
38
+ logger.error(err);
39
+ }
40
+
41
+ process.on("SIGTERM", () => {
42
+ Pyroscope.stop().catch((err: unknown) => {
43
+ logger.error("Error stopping profiler:");
44
+ logger.error(err);
45
+ });
46
+ });
47
+ }
48
+
49
+ private static getServerAddress(): string | undefined {
50
+ /*
51
+ * Use the OTLP endpoint base URL as the Pyroscope server address.
52
+ * The Pyroscope SDK will append /ingest to this URL.
53
+ * The final URL will be /pyroscope/ingest, routed by nginx to the telemetry service.
54
+ */
55
+ const endpoint: string | undefined =
56
+ process.env["OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT"];
57
+
58
+ if (!endpoint) {
59
+ return undefined;
60
+ }
61
+
62
+ /*
63
+ * Strip /otlp suffix if present and append /pyroscope
64
+ * The Pyroscope SDK appends /ingest, so the final URL will be /pyroscope/ingest
65
+ */
66
+ let baseUrl: string = endpoint;
67
+ if (baseUrl.endsWith("/otlp")) {
68
+ baseUrl = baseUrl.substring(0, baseUrl.length - 5);
69
+ }
70
+ if (baseUrl.endsWith("/")) {
71
+ baseUrl = baseUrl.substring(0, baseUrl.length - 1);
72
+ }
73
+
74
+ return `${baseUrl}/pyroscope`;
75
+ }
76
+
77
+ private static getAuthToken(): string | undefined {
78
+ /*
79
+ * Extract the OneUptime token from OTLP headers
80
+ * Format: "x-oneuptime-token=<value>;other-header=value"
81
+ */
82
+ const headersStr: string | undefined =
83
+ process.env["OPENTELEMETRY_EXPORTER_OTLP_HEADERS"];
84
+
85
+ if (!headersStr) {
86
+ return undefined;
87
+ }
88
+
89
+ const parts: Array<string> = headersStr.split(";");
90
+ for (const part of parts) {
91
+ const [key, value]: Array<string | undefined> = part.split("=") as Array<
92
+ string | undefined
93
+ >;
94
+ if (key === "x-oneuptime-token" && value) {
95
+ return value;
96
+ }
97
+ }
98
+
99
+ return undefined;
100
+ }
101
+ }
@@ -1,3 +1,4 @@
1
+ import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
1
2
  import ReturnResult from "../../../Types/IsolatedVM/ReturnResult";
2
3
  import { JSONObject, JSONValue } from "../../../Types/JSON";
3
4
  import axios, { AxiosResponse } from "axios";
@@ -310,6 +311,9 @@ export default class VMRunner {
310
311
  const MAX_LOG_BYTES: number = 1_000_000; // 1MB cap
311
312
  let totalLogBytes: number = 0;
312
313
 
314
+ const capturedMetrics: CapturedMetric[] = [];
315
+ const MAX_METRICS: number = 100;
316
+
313
317
  // Track timer handles so we can clean them up after execution
314
318
  type TimerHandle = ReturnType<typeof setTimeout>;
315
319
  const pendingTimeouts: TimerHandle[] = [];
@@ -398,6 +402,47 @@ export default class VMRunner {
398
402
  proxyCache,
399
403
  );
400
404
 
405
+ sandbox["oneuptime"] = createSandboxProxy(
406
+ {
407
+ captureMetric: (
408
+ name: unknown,
409
+ value: unknown,
410
+ attributes?: unknown,
411
+ ): void => {
412
+ if (typeof name !== "string" || name.length === 0) {
413
+ return;
414
+ }
415
+ if (typeof value !== "number" || isNaN(value)) {
416
+ return;
417
+ }
418
+ if (capturedMetrics.length >= MAX_METRICS) {
419
+ return;
420
+ }
421
+ const metric: CapturedMetric = {
422
+ name: name.substring(0, 200),
423
+ value: value,
424
+ };
425
+ if (attributes && typeof attributes === "object") {
426
+ const safeAttrs: JSONObject = {};
427
+ for (const [k, v] of Object.entries(
428
+ attributes as Record<string, unknown>,
429
+ )) {
430
+ if (
431
+ typeof v === "string" ||
432
+ typeof v === "number" ||
433
+ typeof v === "boolean"
434
+ ) {
435
+ safeAttrs[k] = String(v);
436
+ }
437
+ }
438
+ metric.attributes = safeAttrs;
439
+ }
440
+ capturedMetrics.push(metric);
441
+ },
442
+ },
443
+ proxyCache,
444
+ );
445
+
401
446
  // Wrap any additional context (e.g. Playwright browser/page objects)
402
447
  if (options.context) {
403
448
  for (const key of Object.keys(options.context)) {
@@ -450,6 +495,7 @@ export default class VMRunner {
450
495
  return {
451
496
  returnValue: deepUnwrapProxies(returnVal),
452
497
  logMessages,
498
+ capturedMetrics,
453
499
  };
454
500
  } finally {
455
501
  // Clean up any lingering timers to prevent resource leaks
@@ -474,6 +520,8 @@ export default class VMRunner {
474
520
  const timeout: number = options.timeout || 5000;
475
521
 
476
522
  const logMessages: string[] = [];
523
+ const capturedMetrics: CapturedMetric[] = [];
524
+ const MAX_METRICS: number = 100;
477
525
 
478
526
  const isolate: ivm.Isolate = new ivm.Isolate({ memoryLimit: 128 });
479
527
 
@@ -499,6 +547,45 @@ export default class VMRunner {
499
547
  }))};
500
548
  `);
501
549
 
550
+ // oneuptime.captureMetric - fire-and-forget callback
551
+ await jail.set(
552
+ "_captureMetric",
553
+ new ivm.Callback(
554
+ (name: string, value: string, attributesJson?: string) => {
555
+ if (capturedMetrics.length >= MAX_METRICS) {
556
+ return;
557
+ }
558
+ const numValue: number = Number(value);
559
+ if (isNaN(numValue)) {
560
+ return;
561
+ }
562
+ const metric: CapturedMetric = {
563
+ name: String(name).substring(0, 200),
564
+ value: numValue,
565
+ };
566
+ if (attributesJson) {
567
+ try {
568
+ metric.attributes = JSON.parse(attributesJson) as JSONObject;
569
+ } catch {
570
+ // ignore invalid JSON
571
+ }
572
+ }
573
+ capturedMetrics.push(metric);
574
+ },
575
+ ),
576
+ );
577
+
578
+ await context.eval(`
579
+ const oneuptime = {
580
+ captureMetric: (name, value, attributes) => {
581
+ if (typeof name !== 'string' || name.length === 0) return;
582
+ if (typeof value !== 'number' || isNaN(value)) return;
583
+ const attrJson = attributes ? JSON.stringify(attributes) : undefined;
584
+ _captureMetric(String(name), String(value), attrJson);
585
+ }
586
+ };
587
+ `);
588
+
502
589
  // args - deep copy into isolate
503
590
  if (options.args) {
504
591
  await jail.set("_args", new ivm.ExternalCopy(options.args).copyInto());
@@ -961,6 +1048,7 @@ export default class VMRunner {
961
1048
  return {
962
1049
  returnValue,
963
1050
  logMessages,
1051
+ capturedMetrics,
964
1052
  };
965
1053
  } finally {
966
1054
  if (!isolate.isDisposed) {