@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.
- package/Models/DatabaseModels/Workflow.ts +29 -0
- package/Server/API/StatusPageAPI.ts +48 -0
- package/Server/EnvironmentConfig.ts +5 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +45 -0
- package/Server/Services/IncidentService.ts +81 -13
- package/Server/Services/LogAggregationService.ts +1 -0
- package/Server/Services/WorkflowService.ts +28 -1
- package/Server/Types/Workflow/Components/Webhook.ts +28 -5
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +29 -13
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +163 -26
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +92 -0
- package/Server/Utils/Profiling.ts +101 -0
- package/Server/Utils/VM/VMRunner.ts +88 -0
- package/Types/Dashboard/DashboardTemplates.ts +1149 -0
- package/Types/Exception/ExceptionMetricType.ts +15 -0
- package/Types/IsolatedVM/ReturnResult.ts +3 -0
- package/Types/Metrics/MetricDashboardMetricType.ts +28 -0
- package/Types/Metrics/MetricsQuery.ts +2 -1
- package/Types/Monitor/CustomCodeMonitor/CapturedMetric.ts +7 -0
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +2 -0
- package/Types/Monitor/UptimeBarTooltipIncident.ts +21 -0
- package/Types/Profile/ProfileMetricType.ts +16 -0
- package/Types/Span/SpanMetricType.ts +17 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +40 -33
- package/UI/Components/Charts/Bar/BarChart.tsx +37 -30
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +196 -51
- package/UI/Components/Charts/ChartGroup/NoDataMessage.tsx +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +39 -32
- package/UI/Components/Forms/BasicForm.tsx +1 -1
- package/UI/Components/Graphs/DayUptimeGraph.tsx +88 -35
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +547 -0
- package/UI/Components/MonitorGraphs/Uptime.tsx +7 -0
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +225 -0
- package/UI/Components/RadioButtons/GroupRadioButtons.tsx +1 -0
- package/UI/Components/Tooltip/Tooltip.tsx +29 -4
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +2 -0
- package/UI/Components/Workflow/DocumentationViewer.tsx +5 -0
- package/UI/Components/Workflow/Workflow.tsx +2 -0
- package/Utils/Alerts/AlertMetricType.ts +98 -0
- package/Utils/Incident/IncidentMetricType.ts +130 -0
- package/build/dist/Models/DatabaseModels/Workflow.js +30 -0
- package/build/dist/Models/DatabaseModels/Workflow.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +42 -0
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +2 -2
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774559064920-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +34 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +52 -9
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowService.js +25 -0
- package/build/dist/Server/Services/WorkflowService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Webhook.js +23 -5
- package/build/dist/Server/Types/Workflow/Components/Webhook.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +21 -7
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +120 -21
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +68 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Profiling.js +80 -0
- package/build/dist/Server/Utils/Profiling.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +68 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +1095 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Exception/ExceptionMetricType.js +16 -0
- package/build/dist/Types/Exception/ExceptionMetricType.js.map +1 -0
- package/build/dist/Types/Metrics/MetricDashboardMetricType.js +26 -0
- package/build/dist/Types/Metrics/MetricDashboardMetricType.js.map +1 -0
- package/build/dist/Types/Monitor/CustomCodeMonitor/CapturedMetric.js +2 -0
- package/build/dist/Types/Monitor/CustomCodeMonitor/CapturedMetric.js.map +1 -0
- package/build/dist/Types/Monitor/UptimeBarTooltipIncident.js +2 -0
- package/build/dist/Types/Monitor/UptimeBarTooltipIncident.js.map +1 -0
- package/build/dist/Types/Profile/ProfileMetricType.js +17 -0
- package/build/dist/Types/Profile/ProfileMetricType.js.map +1 -0
- package/build/dist/Types/Span/SpanMetricType.js +18 -0
- package/build/dist/Types/Span/SpanMetricType.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +21 -16
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +20 -15
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +73 -15
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/NoDataMessage.js +7 -0
- package/build/dist/UI/Components/Charts/ChartGroup/NoDataMessage.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +20 -15
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +46 -20
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +303 -0
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -0
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +118 -0
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -0
- package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +13 -3
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js +1 -0
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/Utils/Alerts/AlertMetricType.js +84 -0
- package/build/dist/Utils/Alerts/AlertMetricType.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentMetricType.js +114 -0
- package/build/dist/Utils/Incident/IncidentMetricType.js.map +1 -0
- 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(
|
|
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
|
-
]
|
|
720
|
-
|
|
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
|
|
798
|
+
const cells: Array<string> = [];
|
|
729
799
|
|
|
730
|
-
if (
|
|
731
|
-
|
|
800
|
+
if (hasNamespace) {
|
|
801
|
+
cells.push(resource.namespace ? `\`${resource.namespace}\`` : "-");
|
|
732
802
|
}
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
803
|
+
if (hasWorkload) {
|
|
804
|
+
cells.push(resource.workloadType ? `${resource.workloadType}` : "-");
|
|
805
|
+
cells.push(
|
|
806
|
+
resource.workloadName ? `\`${resource.workloadName}\`` : "-",
|
|
736
807
|
);
|
|
737
808
|
}
|
|
738
|
-
if (
|
|
739
|
-
|
|
809
|
+
if (hasPod) {
|
|
810
|
+
cells.push(resource.podName ? `\`${resource.podName}\`` : "-");
|
|
740
811
|
}
|
|
741
|
-
if (
|
|
742
|
-
|
|
812
|
+
if (hasContainer) {
|
|
813
|
+
cells.push(
|
|
814
|
+
resource.containerName ? `\`${resource.containerName}\`` : "-",
|
|
815
|
+
);
|
|
743
816
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
817
|
+
if (hasNode) {
|
|
818
|
+
cells.push(resource.nodeName ? `\`${resource.nodeName}\`` : "-");
|
|
746
819
|
}
|
|
747
820
|
|
|
748
|
-
|
|
821
|
+
cells.push(`**${resource.metricValue}**`);
|
|
749
822
|
|
|
750
|
-
resourceLines.push(
|
|
823
|
+
resourceLines.push(`| ${cells.join(" | ")} |`);
|
|
751
824
|
}
|
|
752
825
|
|
|
753
826
|
if (sortedResources.length > 10) {
|
|
754
827
|
resourceLines.push(
|
|
755
|
-
|
|
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) {
|