Merge pull request #2360 from OneUptime/k8s-impl

feat: Add Kubernetes Cluster Management and Monitoring Agent
This commit is contained in:
Simon Larsen
2026-03-18 18:54:29 +00:00
committed by GitHub
107 changed files with 7523 additions and 376 deletions

View File

@@ -6,6 +6,13 @@
"runtimeExecutable": "bash",
"runtimeArgs": ["-c", "cd MobileApp && npx expo start --port 8081"],
"port": 8081
},
{
"name": "dashboard",
"runtimeExecutable": "bash",
"runtimeArgs": ["-c", "cd App/FeatureSet/Dashboard && npm run dev"],
"port": 3002,
"autoPort": false
}
]
}

View File

@@ -18,9 +18,10 @@ jobs:
- name: Install Helm
run: |
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Lint Helm Chart
- name: Lint Helm Chart
run: |
helm lint ./HelmChart/Public/oneuptime
helm lint ./HelmChart/Public/kubernetes-agent
js-lint:
runs-on: ubuntu-latest

View File

@@ -134,11 +134,16 @@ jobs:
helm lint oneuptime
helm template oneuptime --values oneuptime/values.yaml
helm package --sign --key 'key@oneuptime.com' --keyring ~/.gnupg/secring.gpg oneuptime --version ${{needs.read-version.outputs.major_minor}} --app-version ${{needs.read-version.outputs.major_minor}}
echo "Helm Chart Package created successfully"
echo "OneUptime Helm Chart Package created successfully"
helm lint kubernetes-agent
helm template kubernetes-agent --values kubernetes-agent/values.yaml
helm package --sign --key 'key@oneuptime.com' --keyring ~/.gnupg/secring.gpg kubernetes-agent --version ${{needs.read-version.outputs.major_minor}} --app-version ${{needs.read-version.outputs.major_minor}}
echo "Kubernetes Agent Helm Chart Package created successfully"
cd ..
ls
echo "Copying the package to helm-chart repo"
rm -r ../../helm-chart/oneuptime
rm -rf ../../helm-chart/oneuptime
rm -rf ../../helm-chart/kubernetes-agent
cp -r ./Public/* ../../helm-chart
echo "Package copied successfully"
cd .. && cd .. && cd helm-chart

View File

@@ -233,6 +233,9 @@ import IncidentTemplateOwnerUserService, {
import IncidentTemplateService, {
Service as IncidentTemplateServiceType,
} from "Common/Server/Services/IncidentTemplateService";
import KubernetesClusterService, {
Service as KubernetesClusterServiceType,
} from "Common/Server/Services/KubernetesClusterService";
import LabelService, {
Service as LabelServiceType,
} from "Common/Server/Services/LabelService";
@@ -552,6 +555,7 @@ import IncidentTemplate from "Common/Models/DatabaseModels/IncidentTemplate";
import IncidentTemplateOwnerTeam from "Common/Models/DatabaseModels/IncidentTemplateOwnerTeam";
import IncidentTemplateOwnerUser from "Common/Models/DatabaseModels/IncidentTemplateOwnerUser";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Label from "Common/Models/DatabaseModels/Label";
import MonitorCustomField from "Common/Models/DatabaseModels/MonitorCustomField";
import MonitorGroupOwnerTeam from "Common/Models/DatabaseModels/MonitorGroupOwnerTeam";
@@ -1859,6 +1863,14 @@ const BaseAPIFeatureSet: FeatureSet = {
new BaseAPI<Label, LabelServiceType>(Label, LabelService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<KubernetesCluster, KubernetesClusterServiceType>(
KubernetesCluster,
KubernetesClusterService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<EmailVerificationToken, EmailVerificationTokenServiceType>(

View File

@@ -181,6 +181,15 @@ const ServiceRoutes: React.LazyExoticComponent<
};
});
});
const KubernetesRoutes: React.LazyExoticComponent<
AllRoutesModule["KubernetesRoutes"]
> = lazy(() => {
return import("./Routes/AllRoutes").then((m: AllRoutesModule) => {
return {
default: m.KubernetesRoutes,
};
});
});
const CodeRepositoryRoutes: React.LazyExoticComponent<
AllRoutesModule["CodeRepositoryRoutes"]
> = lazy(() => {
@@ -528,6 +537,12 @@ const App: () => JSX.Element = () => {
element={<ServiceRoutes {...commonPageProps} />}
/>
{/* Kubernetes */}
<PageRoute
path={RouteMap[PageMap.KUBERNETES_ROOT]?.toString() || ""}
element={<KubernetesRoutes {...commonPageProps} />}
/>
{/* Code Repository */}
<PageRoute
path={RouteMap[PageMap.CODE_REPOSITORY_ROOT]?.toString() || ""}

View File

@@ -29,6 +29,9 @@ export interface ComponentProps {
query: Query<TelemetryException>;
title: string;
description: string;
onFetchSuccess?:
| ((data: Array<TelemetryException>, totalCount: number) => void)
| undefined;
}
const TelemetryExceptionTable: FunctionComponent<ComponentProps> = (
@@ -47,6 +50,7 @@ const TelemetryExceptionTable: FunctionComponent<ComponentProps> = (
userPreferencesKey="telemetry-exception-table"
isEditable={false}
isCreateable={false}
onFetchSuccess={props.onFetchSuccess}
singularName="Exception"
pluralName="Exceptions"
name="TelemetryException"

View File

@@ -0,0 +1,313 @@
import React, {
FunctionComponent,
ReactElement,
useEffect,
useMemo,
useState,
} from "react";
import TelemetryIngestionKey from "Common/Models/DatabaseModels/TelemetryIngestionKey";
import ModelAPI, { ListResult } from "Common/UI/Utils/ModelAPI/ModelAPI";
import ProjectUtil from "Common/UI/Utils/Project";
import { HOST, HTTP_PROTOCOL } from "Common/UI/Config";
import ModelFormModal from "Common/UI/Components/ModelFormModal/ModelFormModal";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import { FormType } from "Common/UI/Components/Forms/ModelForm";
import API from "Common/UI/Utils/API/API";
import IconProp from "Common/Types/Icon/IconProp";
import Icon from "Common/UI/Components/Icon/Icon";
import Dropdown, {
DropdownOption,
DropdownValue,
} from "Common/UI/Components/Dropdown/Dropdown";
import Protocol from "Common/Types/API/Protocol";
import Card from "Common/UI/Components/Card/Card";
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
import { getKubernetesInstallationMarkdown } from "../../Pages/Kubernetes/Utils/DocumentationMarkdown";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
export interface ComponentProps {
clusterName: string;
title: string;
description: string;
}
const KubernetesDocumentationCard: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
// Ingestion key state
const [ingestionKeys, setIngestionKeys] = useState<
Array<TelemetryIngestionKey>
>([]);
const [selectedKeyId, setSelectedKeyId] = useState<string>("");
const [isLoadingKeys, setIsLoadingKeys] = useState<boolean>(true);
const [showCreateModal, setShowCreateModal] = useState<boolean>(false);
const [keyError, setKeyError] = useState<string>("");
// Compute OneUptime URL
const httpProtocol: string =
HTTP_PROTOCOL === Protocol.HTTPS ? "https" : "http";
const oneuptimeUrl: string = HOST
? `${httpProtocol}://${HOST}`
: "<YOUR_ONEUPTIME_URL>";
// Fetch ingestion keys on mount
useEffect(() => {
loadIngestionKeys().catch(() => {});
}, []);
const loadIngestionKeys: () => Promise<void> = async (): Promise<void> => {
try {
setIsLoadingKeys(true);
setKeyError("");
const result: ListResult<TelemetryIngestionKey> =
await ModelAPI.getList<TelemetryIngestionKey>({
modelType: TelemetryIngestionKey,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
limit: 50,
skip: 0,
select: {
_id: true,
name: true,
secretKey: true,
description: true,
},
sort: {},
});
setIngestionKeys(result.data);
// Auto-select the first key if available and none selected
if (result.data.length > 0 && !selectedKeyId) {
setSelectedKeyId(result.data[0]!.id?.toString() || "");
}
} catch (err) {
setKeyError(API.getFriendlyErrorMessage(err as Error));
} finally {
setIsLoadingKeys(false);
}
};
// Get the selected key object
const selectedKey: TelemetryIngestionKey | undefined = useMemo(() => {
return ingestionKeys.find((k: TelemetryIngestionKey) => {
return k.id?.toString() === selectedKeyId;
});
}, [ingestionKeys, selectedKeyId]);
// Get API key for code snippets
const apiKeyValue: string =
selectedKey?.secretKey?.toString() || "<YOUR_API_KEY>";
const renderKeySelector: () => ReactElement = (): ReactElement => {
if (isLoadingKeys) {
return <PageLoader isVisible={true} />;
}
if (keyError) {
return <ErrorMessage message={keyError} />;
}
if (ingestionKeys.length === 0) {
return (
<div className="text-center py-6">
<p className="text-sm font-medium text-gray-900 mb-1">
No ingestion keys yet
</p>
<p className="text-xs text-gray-500 mb-4">
Create an ingestion key to authenticate your Kubernetes agent.
</p>
<button
type="button"
onClick={() => {
setShowCreateModal(true);
}}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors shadow-sm"
>
<Icon icon={IconProp.Add} className="w-4 h-4" />
Create Ingestion Key
</button>
</div>
);
}
return (
<div>
{/* Key selector row */}
<div className="flex items-center gap-2 mb-3">
<div className="flex-1">
<Dropdown
options={ingestionKeys.map(
(key: TelemetryIngestionKey): DropdownOption => {
return {
value: key.id?.toString() || "",
label: key.name || "Unnamed Key",
};
},
)}
value={
ingestionKeys
.filter((key: TelemetryIngestionKey) => {
return key.id?.toString() === selectedKeyId;
})
.map((key: TelemetryIngestionKey): DropdownOption => {
return {
value: key.id?.toString() || "",
label: key.name || "Unnamed Key",
};
})[0]
}
onChange={(
value: DropdownValue | Array<DropdownValue> | null,
) => {
if (value) {
setSelectedKeyId(value.toString());
}
}}
placeholder="Select an ingestion key"
ariaLabel="Select ingestion key"
/>
</div>
<button
type="button"
onClick={() => {
setShowCreateModal(true);
}}
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors flex-shrink-0"
>
<Icon icon={IconProp.Add} className="w-4 h-4" />
New Key
</button>
</div>
{/* Credentials display */}
{selectedKey && (
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="grid grid-cols-1 divide-y divide-gray-100">
<div className="px-4 py-3 flex items-start gap-3">
<div className="w-8 h-8 rounded-md bg-blue-50 flex items-center justify-center flex-shrink-0 mt-0.5">
<Icon
icon={IconProp.Globe}
className="w-4 h-4 text-blue-600"
/>
</div>
<div className="min-w-0 flex-1">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wider">
OneUptime URL
</div>
<div className="text-sm text-gray-900 font-mono mt-0.5 break-all select-all">
{oneuptimeUrl}
</div>
</div>
</div>
<div className="px-4 py-3 flex items-start gap-3">
<div className="w-8 h-8 rounded-md bg-amber-50 flex items-center justify-center flex-shrink-0 mt-0.5">
<Icon
icon={IconProp.Key}
className="w-4 h-4 text-amber-600"
/>
</div>
<div className="min-w-0 flex-1">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wider">
API Key
</div>
<div className="text-sm text-gray-900 font-mono mt-0.5 break-all select-all">
{selectedKey.secretKey?.toString() || "—"}
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
const installationMarkdown: string = getKubernetesInstallationMarkdown({
clusterName: props.clusterName,
oneuptimeUrl: oneuptimeUrl,
apiKey: apiKeyValue,
});
return (
<div>
<Card title={props.title} description={props.description}>
<div className="px-4 pb-6">
{/* Ingestion Key Section */}
<div className="mb-6">
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-2">
Select Ingestion Key
</label>
{renderKeySelector()}
</div>
{/* Documentation */}
<MarkdownViewer text={installationMarkdown} />
</div>
</Card>
{/* Create Ingestion Key Modal */}
{showCreateModal && (
<ModelFormModal<TelemetryIngestionKey>
modelType={TelemetryIngestionKey}
name="Create Ingestion Key"
title="Create Ingestion Key"
description="Create a new telemetry ingestion key for authenticating your Kubernetes agent."
onClose={() => {
setShowCreateModal(false);
}}
submitButtonText="Create Key"
onSuccess={(item: TelemetryIngestionKey) => {
setShowCreateModal(false);
loadIngestionKeys()
.then(() => {
if (item.id) {
setSelectedKeyId(item.id.toString());
}
})
.catch(() => {});
}}
formProps={{
name: "Create Ingestion Key",
modelType: TelemetryIngestionKey,
id: "create-ingestion-key",
fields: [
{
field: {
name: true,
},
title: "Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "e.g. Kubernetes Agent Key",
validation: {
minLength: 2,
},
},
{
field: {
description: true,
},
title: "Description",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "Optional description for this key",
},
],
formType: FormType.Create,
}}
onBeforeCreate={(
item: TelemetryIngestionKey,
): Promise<TelemetryIngestionKey> => {
item.projectId = ProjectUtil.getCurrentProjectId()!;
return Promise.resolve(item);
}}
/>
)}
</div>
);
};
export default KubernetesDocumentationCard;

View File

@@ -70,6 +70,8 @@ export interface ComponentProps {
noLogsMessage?: string | undefined;
logQuery?: Query<Log> | undefined;
limit?: number | undefined;
onCountChange?: ((count: number) => void) | undefined;
onShowDocumentation?: (() => void) | undefined;
}
const DEFAULT_PAGE_SIZE: number = 100;
@@ -427,6 +429,10 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
setLogs(listResult.data);
setTotalCount(listResult.count);
if (props.onCountChange) {
props.onCountChange(listResult.count);
}
const maximumPage: number = Math.max(
1,
Math.ceil(listResult.count / Math.max(pageSize, 1)),
@@ -1236,6 +1242,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
onFieldValueSelect={handleFieldValueSelect}
timeRange={timeRange}
onTimeRangeChange={handleTimeRangeChange}
onShowDocumentation={props.onShowDocumentation}
selectedColumns={selectedColumns}
onSelectedColumnsChange={(columns: Array<string>) => {
setSelectedColumns(normalizeLogsTableColumns(columns));

View File

@@ -17,6 +17,9 @@ import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"
export interface ComponentProps {
serviceIds?: Array<ObjectID> | undefined;
onFetchSuccess?:
| ((data: Array<MetricType>, totalCount: number) => void)
| undefined;
}
const MetricsTable: FunctionComponent<ComponentProps> = (
@@ -113,6 +116,7 @@ const MetricsTable: FunctionComponent<ComponentProps> = (
}}
showViewIdButton={false}
noItemsMessage={"No metrics found for this service."}
onFetchSuccess={props.onFetchSuccess}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[

View File

@@ -144,6 +144,17 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
iconColor: "indigo",
category: "Observability",
},
// {
// title: "Kubernetes",
// description: "Monitor Kubernetes clusters.",
// route: RouteUtil.populateRouteParams(
// RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
// ),
// activeRoute: RouteMap[PageMap.KUBERNETES_CLUSTERS],
// icon: IconProp.Kubernetes,
// iconColor: "blue",
// category: "Observability",
// },
// Automation & Analytics
{
title: "Dashboards",

View File

@@ -622,8 +622,7 @@ const SpanViewer: FunctionComponent<ComponentProps> = (
</code>
</div>
</div>
{link.attributes &&
Object.keys(link.attributes).length > 0 ? (
{link.attributes && Object.keys(link.attributes).length > 0 ? (
<div>
<div className="text-xs text-gray-500 font-medium mb-1">
Attributes

View File

@@ -37,7 +37,8 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
const [hoveredSpanId, setHoveredSpanId] = React.useState<string | null>(null);
const [focusedSpanId, setFocusedSpanId] = React.useState<string | null>(null);
const containerRef: React.RefObject<HTMLDivElement | null> = React.useRef<HTMLDivElement>(null);
const containerRef: React.RefObject<HTMLDivElement | null> =
React.useRef<HTMLDivElement>(null);
// Build span data for critical path utility
const spanDataList: SpanData[] = React.useMemo(() => {
@@ -90,7 +91,7 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
}
}
const getServiceInfo = (
const getServiceInfo: (span: Span) => { color: Color; name: string } = (
span: Span,
): { color: Color; name: string } => {
const service: Service | undefined = telemetryServices.find(
@@ -104,7 +105,10 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
};
};
const buildNode = (span: Span, depth: number): FlameGraphNode => {
const buildNode: (span: Span, depth: number) => FlameGraphNode = (
span: Span,
depth: number,
): FlameGraphNode => {
const children: Span[] = childrenMap.get(span.spanId!) || [];
const selfTime: SpanSelfTime | undefined = selfTimes.get(span.spanId!);
const serviceInfo: { color: Color; name: string } = getServiceInfo(span);
@@ -123,7 +127,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
startTimeUnixNano: span.startTimeUnixNano!,
endTimeUnixNano: span.endTimeUnixNano!,
durationUnixNano: span.durationUnixNano!,
selfTimeUnixNano: selfTime ? selfTime.selfTimeUnixNano : span.durationUnixNano!,
selfTimeUnixNano: selfTime
? selfTime.selfTimeUnixNano
: span.durationUnixNano!,
serviceColor: serviceInfo.color,
serviceName: serviceInfo.name,
};
@@ -155,7 +161,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
// Find max depth for height calculation
const maxDepth: number = React.useMemo(() => {
let max: number = 0;
const traverse = (node: FlameGraphNode): void => {
const traverse: (node: FlameGraphNode) => void = (
node: FlameGraphNode,
): void => {
if (node.depth > max) {
max = node.depth;
}
@@ -175,7 +183,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
return { viewStart: traceStart, viewEnd: traceEnd };
}
const findNode = (nodes: FlameGraphNode[]): FlameGraphNode | null => {
const findNode: (nodes: FlameGraphNode[]) => FlameGraphNode | null = (
nodes: FlameGraphNode[],
): FlameGraphNode | null => {
for (const node of nodes) {
if (node.span.spanId === focusedSpanId) {
return node;
@@ -210,7 +220,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
);
}
const renderNode = (node: FlameGraphNode): ReactElement | null => {
const renderNode: (node: FlameGraphNode) => ReactElement | null = (
node: FlameGraphNode,
): ReactElement | null => {
// Calculate position relative to view
const nodeStart: number = Math.max(node.startTimeUnixNano, viewStart);
const nodeEnd: number = Math.min(node.endTimeUnixNano, viewEnd);
@@ -220,13 +232,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
}
const leftPercent: number =
totalDuration > 0
? ((nodeStart - viewStart) / totalDuration) * 100
: 0;
totalDuration > 0 ? ((nodeStart - viewStart) / totalDuration) * 100 : 0;
const widthPercent: number =
totalDuration > 0
? ((nodeEnd - nodeStart) / totalDuration) * 100
: 0;
totalDuration > 0 ? ((nodeEnd - nodeStart) / totalDuration) * 100 : 0;
const isHovered: boolean = hoveredSpanId === node.span.spanId;
const isSelected: boolean = selectedSpanId === node.span.spanId;
@@ -302,7 +310,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
if (!hoveredSpanId) {
return null;
}
const findNode = (nodes: FlameGraphNode[]): FlameGraphNode | null => {
const findNode: (nodes: FlameGraphNode[]) => FlameGraphNode | null = (
nodes: FlameGraphNode[],
): FlameGraphNode | null => {
for (const node of nodes) {
if (node.span.spanId === hoveredSpanId) {
return node;

View File

@@ -712,7 +712,13 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
});
}
return filtered;
}, [spans, showErrorsOnly, selectedServiceIds, spanSearchText, telemetryServices]);
}, [
spans,
showErrorsOnly,
selectedServiceIds,
spanSearchText,
telemetryServices,
]);
// Search match count for display
const searchMatchCount: number = React.useMemo(() => {
@@ -1288,11 +1294,9 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
<button
type="button"
onClick={() => {
setShowCriticalPath(
(prev: boolean) => {
return !prev;
},
);
setShowCriticalPath((prev: boolean) => {
return !prev;
});
}}
className={`text-xs font-medium px-3 py-1.5 rounded-md border transition-all flex items-center space-x-1 ${
showCriticalPath
@@ -1428,17 +1432,14 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
</div>
<div className="space-y-1.5">
{serviceBreakdown.map((breakdown: ServiceBreakdown) => {
const service: Service | undefined =
telemetryServices.find((s: Service) => {
return (
s._id?.toString() === breakdown.serviceId
);
});
const serviceName: string =
service?.name || "Unknown";
const service: Service | undefined = telemetryServices.find(
(s: Service) => {
return s._id?.toString() === breakdown.serviceId;
},
);
const serviceName: string = service?.name || "Unknown";
const serviceColor: string = String(
(service?.serviceColor as unknown as string) ||
"#6366f1",
(service?.serviceColor as unknown as string) || "#6366f1",
);
const percent: number = Math.min(
breakdown.percentOfTrace,
@@ -1446,7 +1447,10 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
);
return (
<div key={breakdown.serviceId} className="flex items-center space-x-2">
<div
key={breakdown.serviceId}
className="flex items-center space-x-2"
>
<span
className="h-2.5 w-2.5 rounded-sm ring-1 ring-black/10 flex-shrink-0"
style={{
@@ -1468,8 +1472,7 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
</div>
<span className="text-[10px] text-gray-500 w-20 text-right">
{SpanUtil.getSpanDurationAsString({
spanDurationInUnixNano:
breakdown.selfTimeUnixNano,
spanDurationInUnixNano: breakdown.selfTimeUnixNano,
divisibilityFactor: divisibilityFactor,
})}{" "}
({percent.toFixed(1)}%)
@@ -1526,9 +1529,7 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
setSelectedSpans([spanId]);
}}
selectedSpanId={
selectedSpans.length > 0
? selectedSpans[0]
: undefined
selectedSpans.length > 0 ? selectedSpans[0] : undefined
}
/>
</div>

View File

@@ -125,7 +125,8 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
return maxEnd - minStart;
}, [spans]);
const divisibilityFactor = SpanUtil.getDivisibilityFactor(traceDuration);
const divisibilityFactor: number =
SpanUtil.getDivisibilityFactor(traceDuration);
if (nodes.length === 0) {
return (
@@ -135,8 +136,10 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
);
}
// Layout: arrange nodes in a topological order based on edges
// Simple layout: find entry nodes and lay out left-to-right
/*
* Layout: arrange nodes in a topological order based on edges
* Simple layout: find entry nodes and lay out left-to-right
*/
const { nodePositions, layoutWidth, layoutHeight } = React.useMemo(() => {
// Build adjacency list
const adjList: Map<string, string[]> = new Map();
@@ -151,10 +154,7 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
const neighbors: string[] = adjList.get(edge.fromServiceId) || [];
neighbors.push(edge.toServiceId);
adjList.set(edge.fromServiceId, neighbors);
inDegree.set(
edge.toServiceId,
(inDegree.get(edge.toServiceId) || 0) + 1,
);
inDegree.set(edge.toServiceId, (inDegree.get(edge.toServiceId) || 0) + 1);
}
// Topological sort using BFS (Kahn's algorithm)
@@ -333,18 +333,16 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
refY="3"
orient="auto"
>
<polygon
points="0 0, 8 3, 0 6"
fill="#9ca3af"
/>
<polygon points="0 0, 8 3, 0 6" fill="#9ca3af" />
</marker>
</defs>
</svg>
{/* Render nodes */}
{nodes.map((node: ServiceNode) => {
const pos: { x: number; y: number } | undefined =
nodePositions.get(node.serviceId);
const pos: { x: number; y: number } | undefined = nodePositions.get(
node.serviceId,
);
if (!pos) {
return null;
}
@@ -355,9 +353,7 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
<div
key={node.serviceId}
className={`absolute rounded-lg border-2 bg-white shadow-sm p-3 ${
hasErrors
? "border-red-300"
: "border-gray-200"
hasErrors ? "border-red-300" : "border-gray-200"
}`}
style={{
left: `${pos.x}px`,

View File

@@ -44,6 +44,9 @@ export interface ComponentProps {
spanQuery?: Query<Span> | undefined;
isMinimalTable?: boolean | undefined;
noItemsMessage?: string | undefined;
onFetchSuccess?:
| ((data: Array<Span>, totalCount: number) => void)
| undefined;
}
const TraceTable: FunctionComponent<ComponentProps> = (
@@ -298,6 +301,7 @@ const TraceTable: FunctionComponent<ComponentProps> = (
noItemsMessage={
props.noItemsMessage ? props.noItemsMessage : "No spans found."
}
onFetchSuccess={props.onFetchSuccess}
showRefreshButton={true}
sortBy="startTime"
sortOrder={SortOrder.Descending}
@@ -310,12 +314,9 @@ const TraceTable: FunctionComponent<ComponentProps> = (
);
}
return Promise.resolve(
RouteUtil.populateRouteParams(
RouteMap[PageMap.TRACE_VIEW]!,
{
modelId: span.traceId!.toString(),
},
),
RouteUtil.populateRouteParams(RouteMap[PageMap.TRACE_VIEW]!, {
modelId: span.traceId!.toString(),
}),
);
}}
filters={[

View File

@@ -0,0 +1,11 @@
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
const ExceptionsDocumentationPage: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps,
): ReactElement => {
return <TelemetryDocumentation telemetryType="exceptions" />;
};
export default ExceptionsDocumentationPage;

View File

@@ -51,6 +51,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
},
],
},
{
title: "Documentation",
items: [
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];
return <SideMenu sections={sections} />;

View File

@@ -1,7 +1,18 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ExceptionsTable from "../../Components/Exceptions/ExceptionsTable";
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 UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -9,12 +20,48 @@ const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
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 (
<ExceptionsTable
query={{

View File

@@ -0,0 +1,180 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import PageComponentProps from "../PageComponentProps";
import Route from "Common/Types/API/Route";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import FieldType from "Common/UI/Components/Types/FieldType";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import KubernetesDocumentationCard from "../../Components/Kubernetes/DocumentationCard";
const KubernetesClusters: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const [clusterCount, setClusterCount] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchClusterCount: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const count: number = await ModelAPI.count({
modelType: KubernetesCluster,
query: {},
});
setClusterCount(count);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchClusterCount().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (clusterCount === 0) {
return (
<Fragment>
<KubernetesDocumentationCard
clusterName="my-cluster"
title="Getting Started with Kubernetes Monitoring"
description="No Kubernetes clusters connected yet. Install the agent using the guide below and your cluster will appear here automatically."
/>
</Fragment>
);
}
return (
<Fragment>
<ModelTable<KubernetesCluster>
modelType={KubernetesCluster}
id="kubernetes-clusters-table"
userPreferencesKey="kubernetes-clusters-table"
isDeleteable={false}
isEditable={false}
isCreateable={true}
name="Kubernetes Clusters"
isViewable={true}
filters={[]}
cardProps={{
title: "Kubernetes Clusters",
description:
"Clusters being monitored in this project. Install the OneUptime kubernetes-agent Helm chart to connect a cluster.",
}}
showViewIdButton={true}
formFields={[
{
field: {
name: true,
},
title: "Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "production-us-east",
},
{
field: {
clusterIdentifier: true,
},
title: "Cluster Identifier",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "production-us-east-1",
description:
"This should match the clusterName value in your kubernetes-agent Helm chart.",
},
{
field: {
description: true,
},
title: "Description",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "Production cluster running in US East",
},
]}
columns={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
clusterIdentifier: true,
},
title: "Cluster Identifier",
type: FieldType.Text,
},
{
field: {
otelCollectorStatus: true,
},
title: "Status",
type: FieldType.Text,
},
{
field: {
nodeCount: true,
},
title: "Nodes",
type: FieldType.Number,
},
{
field: {
podCount: true,
},
title: "Pods",
type: FieldType.Number,
},
{
field: {
provider: true,
},
title: "Provider",
type: FieldType.Text,
},
]}
onViewPage={(item: KubernetesCluster): Promise<Route> => {
return Promise.resolve(
new Route(
RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
{
modelId: item._id,
},
).toString(),
),
);
}}
/>
</Fragment>
);
};
export default KubernetesClusters;

View File

@@ -0,0 +1,19 @@
import PageComponentProps from "../PageComponentProps";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import KubernetesDocumentationCard from "../../Components/Kubernetes/DocumentationCard";
const KubernetesDocumentation: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return (
<Fragment>
<KubernetesDocumentationCard
clusterName="my-cluster"
title="Agent Installation Guide"
description="Install the OneUptime Kubernetes Agent using Helm to connect your cluster. Once installed, the cluster will appear automatically."
/>
</Fragment>
);
};
export default KubernetesDocumentation;

View File

@@ -0,0 +1,25 @@
import { getKubernetesBreadcrumbs } from "../../Utils/Breadcrumbs";
import { RouteUtil } from "../../Utils/RouteMap";
import LayoutPageComponentProps from "../LayoutPageComponentProps";
import SideMenu from "./SideMenu";
import Page from "Common/UI/Components/Page/Page";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import { Outlet } from "react-router-dom";
const KubernetesLayout: FunctionComponent<LayoutPageComponentProps> = (
_props: LayoutPageComponentProps,
): ReactElement => {
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
return (
<Page
title={"Kubernetes"}
sideMenu={<SideMenu />}
breadcrumbLinks={getKubernetesBreadcrumbs(path)}
>
<Outlet />
</Page>
);
};
export default KubernetesLayout;

View File

@@ -0,0 +1,40 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import SideMenu, {
SideMenuSectionProps,
} from "Common/UI/Components/SideMenu/SideMenu";
import React, { FunctionComponent, ReactElement } from "react";
const KubernetesSideMenu: FunctionComponent = (): ReactElement => {
const sections: SideMenuSectionProps[] = [
{
title: "Kubernetes",
items: [
{
link: {
title: "All Clusters",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
),
},
icon: IconProp.List,
},
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];
return <SideMenu sections={sections} />;
};
export default KubernetesSideMenu;

View File

@@ -0,0 +1,144 @@
export interface KubernetesInstallationMarkdownOptions {
clusterName: string;
oneuptimeUrl: string;
apiKey: string;
}
export function getKubernetesInstallationMarkdown(
options: KubernetesInstallationMarkdownOptions,
): string {
const { clusterName, oneuptimeUrl, apiKey } = options;
return `
## Prerequisites
- A running Kubernetes cluster (v1.23+)
- \`kubectl\` configured to access your cluster
- \`helm\` v3 installed
## Step 1: Add the OneUptime Helm Repository
\`\`\`bash
helm repo add oneuptime https://helm.oneuptime.com
helm repo update
\`\`\`
## Step 2: Install the Kubernetes Agent
\`\`\`bash
helm install kubernetes-agent oneuptime/kubernetes-agent \\
--namespace oneuptime-agent \\
--create-namespace \\
--set oneuptime.url="${oneuptimeUrl}" \\
--set oneuptime.apiKey="${apiKey}" \\
--set clusterName="${clusterName}"
\`\`\`
## Step 3: Verify the Installation
Check that the agent pods are running:
\`\`\`bash
kubectl get pods -n oneuptime-agent
\`\`\`
You should see a **Deployment** pod (for metrics and events collection) and **DaemonSet** pods (one per node, for log collection):
\`\`\`
NAME READY STATUS RESTARTS AGE
kubernetes-agent-deployment-xxxxx-xxxxx 1/1 Running 0 1m
kubernetes-agent-daemonset-xxxxx 1/1 Running 0 1m
\`\`\`
Once the agent connects, your cluster will appear automatically in the Kubernetes section.
## Configuration Options
### Namespace Filtering
By default, \`kube-system\` is excluded. To monitor only specific namespaces:
\`\`\`bash
helm install kubernetes-agent oneuptime/kubernetes-agent \\
--namespace oneuptime-agent \\
--create-namespace \\
--set oneuptime.url="${oneuptimeUrl}" \\
--set oneuptime.apiKey="${apiKey}" \\
--set clusterName="${clusterName}" \\
--set "namespaceFilters.include={default,production,staging}"
\`\`\`
### Disable Log Collection
If you only need metrics and events (no pod logs):
\`\`\`bash
helm install kubernetes-agent oneuptime/kubernetes-agent \\
--namespace oneuptime-agent \\
--create-namespace \\
--set oneuptime.url="${oneuptimeUrl}" \\
--set oneuptime.apiKey="${apiKey}" \\
--set clusterName="${clusterName}" \\
--set logs.enabled=false
\`\`\`
### Enable Control Plane Monitoring
For self-managed clusters (not EKS/GKE/AKS), you can enable control plane metrics:
\`\`\`bash
helm install kubernetes-agent oneuptime/kubernetes-agent \\
--namespace oneuptime-agent \\
--create-namespace \\
--set oneuptime.url="${oneuptimeUrl}" \\
--set oneuptime.apiKey="${apiKey}" \\
--set clusterName="${clusterName}" \\
--set controlPlane.enabled=true
\`\`\`
> **Note:** Managed Kubernetes services (EKS, GKE, AKS) typically do not expose control plane metrics. Only enable this for self-managed clusters.
## Upgrading the Agent
\`\`\`bash
helm repo update
helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
--namespace oneuptime-agent
\`\`\`
## Uninstalling the Agent
\`\`\`bash
helm uninstall kubernetes-agent --namespace oneuptime-agent
kubectl delete namespace oneuptime-agent
\`\`\`
## What Gets Collected
The OneUptime Kubernetes Agent collects:
| Category | Data |
|----------|------|
| **Node Metrics** | CPU utilization, memory usage, filesystem usage, network I/O |
| **Pod Metrics** | CPU usage, memory usage, network I/O, restarts |
| **Container Metrics** | CPU usage, memory usage per container |
| **Cluster Metrics** | Node conditions, allocatable resources, pod counts |
| **Kubernetes Events** | Warnings, errors, scheduling events |
| **Pod Logs** | stdout/stderr logs from all containers (via DaemonSet) |
## Troubleshooting
### Agent shows "Disconnected"
1. Check that the agent pods are running: \`kubectl get pods -n oneuptime-agent\`
2. Check the agent logs: \`kubectl logs -n oneuptime-agent deployment/kubernetes-agent-deployment\`
3. Verify your OneUptime URL and API key are correct
4. Ensure your cluster can reach the OneUptime instance over the network
### No metrics appearing
1. Check that the cluster identifier matches: this cluster uses **\`${clusterName}\`**
2. Verify the RBAC permissions: \`kubectl get clusterrolebinding | grep kubernetes-agent\`
3. Check the OTel collector logs for export errors
`;
}

View File

@@ -0,0 +1,314 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
const KubernetesClusterControlPlane: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [etcdMetricViewData, setEtcdMetricViewData] =
useState<MetricViewData | null>(null);
const [apiServerMetricViewData, setApiServerMetricViewData] =
useState<MetricViewData | null>(null);
const [schedulerMetricViewData, setSchedulerMetricViewData] =
useState<MetricViewData | null>(null);
const [controllerMetricViewData, setControllerMetricViewData] =
useState<MetricViewData | null>(null);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
useEffect(() => {
if (!cluster) {
return;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const etcdDbSizeQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "etcd_db_size",
title: "etcd Database Size",
description: "Total size of the etcd database",
legend: "DB Size",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "etcd_mvcc_db_total_size_in_bytes",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const apiServerRequestRateQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "apiserver_requests",
title: "API Server Request Rate",
description: "Total API server requests by verb",
legend: "Requests",
legendUnit: "req/s",
},
metricQueryData: {
filterData: {
metricName: "apiserver_request_total",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Sum,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const apiServerLatencyQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "apiserver_latency",
title: "API Server Request Latency",
description: "API server request duration",
legend: "Latency",
legendUnit: "seconds",
},
metricQueryData: {
filterData: {
metricName: "apiserver_request_duration_seconds",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const schedulerPendingQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "scheduler_pending",
title: "Scheduler Pending Pods",
description: "Number of pods pending scheduling",
legend: "Pending Pods",
legendUnit: "",
},
metricQueryData: {
filterData: {
metricName: "scheduler_pending_pods",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const schedulerLatencyQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "scheduler_latency",
title: "Scheduler Latency",
description: "End-to-end scheduling latency",
legend: "Latency",
legendUnit: "seconds",
},
metricQueryData: {
filterData: {
metricName: "scheduler_e2e_scheduling_duration_seconds",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const controllerQueueDepthQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "controller_queue",
title: "Controller Manager Queue Depth",
description: "Work queue depth for controller manager",
legend: "Queue Depth",
legendUnit: "",
},
metricQueryData: {
filterData: {
metricName: "workqueue_depth",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
setEtcdMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [etcdDbSizeQuery],
formulaConfigs: [],
});
setApiServerMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [apiServerRequestRateQuery, apiServerLatencyQuery],
formulaConfigs: [],
});
setSchedulerMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [schedulerPendingQuery, schedulerLatencyQuery],
formulaConfigs: [],
});
setControllerMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [controllerQueueDepthQuery],
formulaConfigs: [],
});
}, [cluster]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (
!cluster ||
!etcdMetricViewData ||
!apiServerMetricViewData ||
!schedulerMetricViewData ||
!controllerMetricViewData
) {
return <ErrorMessage message="Cluster not found." />;
}
return (
<Fragment>
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-700">
Control plane metrics require the <code>controlPlane.enabled</code>{" "}
flag to be set to <code>true</code> in the kubernetes-agent Helm chart
values. This is typically only available for self-managed Kubernetes
clusters, not managed services like EKS, GKE, or AKS.
</p>
</div>
<Card
title="etcd"
description="etcd is the consistent, distributed key-value store used as the backing store for all cluster data."
>
<MetricView
data={etcdMetricViewData}
hideQueryElements={true}
onChange={() => {}}
/>
</Card>
<Card
title="API Server"
description="The Kubernetes API server validates and configures data for API objects and serves REST operations."
>
<MetricView
data={apiServerMetricViewData}
hideQueryElements={true}
onChange={() => {}}
/>
</Card>
<Card
title="Scheduler"
description="The scheduler watches for newly created pods that have no node assigned and selects a node for them to run on."
>
<MetricView
data={schedulerMetricViewData}
hideQueryElements={true}
onChange={() => {}}
/>
</Card>
<Card
title="Controller Manager"
description="The controller manager runs controller processes that regulate the state of the cluster."
>
<MetricView
data={controllerMetricViewData}
hideQueryElements={true}
onChange={() => {}}
/>
</Card>
</Fragment>
);
};
export default KubernetesClusterControlPlane;

View File

@@ -0,0 +1,34 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import PageComponentProps from "../../PageComponentProps";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import ModelDelete from "Common/UI/Components/ModelDelete/ModelDelete";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
const KubernetesClusterDelete: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<Fragment>
<ModelDelete
modelType={KubernetesCluster}
modelId={modelId}
onDeleteSuccess={() => {
Navigation.navigate(
RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
{ modelId },
),
);
}}
/>
</Fragment>
);
};
export default KubernetesClusterDelete;

View File

@@ -0,0 +1,78 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import KubernetesDocumentationCard from "../../../Components/Kubernetes/DocumentationCard";
const KubernetesClusterDocumentation: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
name: true,
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const clusterName: string =
cluster.clusterIdentifier || cluster.name || "my-cluster";
return (
<Fragment>
<KubernetesDocumentationCard
clusterName={clusterName}
title="Agent Installation Guide"
description="Follow these steps to install the OneUptime Kubernetes Agent on your cluster."
/>
</Fragment>
);
};
export default KubernetesClusterDocumentation;

View File

@@ -0,0 +1,339 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import AnalyticsModelAPI, {
ListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import Log from "Common/Models/AnalyticsModels/Log";
import ProjectUtil from "Common/UI/Utils/Project";
import OneUptimeDate from "Common/Types/Date";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import { JSONObject } from "Common/Types/JSON";
import InBetween from "Common/Types/BaseDatabase/InBetween";
interface KubernetesEvent {
timestamp: string;
type: string;
reason: string;
objectKind: string;
objectName: string;
namespace: string;
message: string;
}
const KubernetesClusterEvents: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [events, setEvents] = useState<Array<KubernetesEvent>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchData: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
if (!item?.clusterIdentifier) {
setIsLoading(false);
return;
}
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
const listResult: ListResult<Log> = await AnalyticsModelAPI.getList<Log>({
modelType: Log,
query: {
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
time: new InBetween<Date>(startDate, endDate),
},
limit: 200,
skip: 0,
select: {
time: true,
body: true,
severityText: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
});
// Helper to extract a string value from OTLP kvlistValue
const getKvValue: (
kvList: JSONObject | undefined,
key: string,
) => string = (kvList: JSONObject | undefined, key: string): string => {
if (!kvList) {
return "";
}
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
"values"
] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === key) {
const val: JSONObject | undefined = entry["value"] as
| JSONObject
| undefined;
if (!val) {
return "";
}
if (val["stringValue"]) {
return val["stringValue"] as string;
}
if (val["intValue"]) {
return String(val["intValue"]);
}
// Nested kvlist (e.g., regarding, metadata)
if (val["kvlistValue"]) {
return val["kvlistValue"] as unknown as string;
}
}
}
return "";
};
// Helper to get nested kvlist value
const getNestedKvValue: (
kvList: JSONObject | undefined,
parentKey: string,
childKey: string,
) => string = (
kvList: JSONObject | undefined,
parentKey: string,
childKey: string,
): string => {
if (!kvList) {
return "";
}
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
"values"
] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === parentKey) {
const val: JSONObject | undefined = entry["value"] as
| JSONObject
| undefined;
if (val && val["kvlistValue"]) {
return getKvValue(val["kvlistValue"] as JSONObject, childKey);
}
}
}
return "";
};
const k8sEvents: Array<KubernetesEvent> = [];
for (const log of listResult.data) {
const attrs: JSONObject = log.attributes || {};
// Filter to only k8s events from this cluster
if (
attrs["resource.k8s.cluster.name"] !== item.clusterIdentifier &&
attrs["k8s.cluster.name"] !== item.clusterIdentifier
) {
continue;
}
// Only process k8s event logs (from k8sobjects receiver)
if (attrs["logAttributes.event.domain"] !== "k8s") {
continue;
}
// Parse the body which is OTLP kvlistValue JSON
let bodyObj: JSONObject | null = null;
try {
if (typeof log.body === "string") {
bodyObj = JSON.parse(log.body) as JSONObject;
}
} catch {
continue;
}
if (!bodyObj) {
continue;
}
// The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
| JSONObject
| undefined;
if (!topKvList) {
continue;
}
// Get the "object" which is the actual k8s Event
const objectKvListRaw: string = getKvValue(topKvList, "object");
if (!objectKvListRaw || typeof objectKvListRaw === "string") {
continue;
}
const objectKvList: JSONObject =
objectKvListRaw as unknown as JSONObject;
const eventType: string = getKvValue(objectKvList, "type") || "";
const reason: string = getKvValue(objectKvList, "reason") || "";
const note: string = getKvValue(objectKvList, "note") || "";
// Get object details from "regarding" sub-object
const objectKind: string =
getNestedKvValue(objectKvList, "regarding", "kind") || "";
const objectName: string =
getNestedKvValue(objectKvList, "regarding", "name") || "";
const namespace: string =
getNestedKvValue(objectKvList, "regarding", "namespace") ||
getNestedKvValue(objectKvList, "metadata", "namespace") ||
"";
if (eventType || reason) {
k8sEvents.push({
timestamp: log.time
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
: "",
type: eventType || "Unknown",
reason: reason || "Unknown",
objectKind: objectKind || "Unknown",
objectName: objectName || "Unknown",
namespace: namespace || "default",
message: note || "",
});
}
}
setEvents(k8sEvents);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchData().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
return (
<Fragment>
<Card
title="Kubernetes Events"
description="Events from the last 24 hours collected by the k8sobjects receiver. Warning events may indicate issues that need attention."
>
{events.length === 0 ? (
<p className="text-gray-500 text-sm">
No Kubernetes events found in the last 24 hours. Events will appear
here once the kubernetes-agent is sending data.
</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Time
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reason
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Object
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Namespace
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Message
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{events.map((event: KubernetesEvent, index: number) => {
const isWarning: boolean =
event.type.toLowerCase() === "warning";
return (
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{event.timestamp}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
isWarning
? "bg-yellow-100 text-yellow-800"
: "bg-green-100 text-green-800"
}`}
>
{event.type}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{event.reason}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{event.objectKind}/{event.objectName}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{event.namespace}
</td>
<td className="px-4 py-3 text-sm text-gray-500 max-w-md truncate">
{event.message}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</Card>
</Fragment>
);
};
export default KubernetesClusterEvents;

View File

@@ -0,0 +1,215 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FieldType from "Common/UI/Components/Types/FieldType";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
const KubernetesClusterOverview: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
name: true,
clusterIdentifier: true,
provider: true,
otelCollectorStatus: true,
lastSeenAt: true,
nodeCount: true,
podCount: true,
namespaceCount: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const statusColor: string =
cluster.otelCollectorStatus === "connected"
? "text-green-600"
: "text-red-600";
return (
<Fragment>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
<InfoCard
title="Nodes"
value={
<span className="text-2xl font-semibold">
{cluster.nodeCount?.toString() || "0"}
</span>
}
/>
<InfoCard
title="Pods"
value={
<span className="text-2xl font-semibold">
{cluster.podCount?.toString() || "0"}
</span>
}
/>
<InfoCard
title="Namespaces"
value={
<span className="text-2xl font-semibold">
{cluster.namespaceCount?.toString() || "0"}
</span>
}
/>
<InfoCard
title="Agent Status"
value={
<span className={`text-2xl font-semibold ${statusColor}`}>
{cluster.otelCollectorStatus === "connected"
? "Connected"
: "Disconnected"}
</span>
}
/>
</div>
{/* Cluster Details */}
<CardModelDetail<KubernetesCluster>
name="Cluster Details"
cardProps={{
title: "Cluster Details",
description: "Basic information about this Kubernetes cluster.",
}}
isEditable={true}
editButtonText="Edit Cluster"
formFields={[
{
field: {
name: true,
},
title: "Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "production-us-east",
},
{
field: {
description: true,
},
title: "Description",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "Production cluster running in US East",
},
{
field: {
clusterIdentifier: true,
},
title: "Cluster Identifier",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "production-us-east-1",
description:
"This should match the clusterName value in your kubernetes-agent Helm chart.",
},
{
field: {
provider: true,
},
title: "Provider",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "EKS, GKE, AKS, etc.",
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 2,
modelType: KubernetesCluster,
id: "kubernetes-cluster-overview",
modelId: modelId,
fields: [
{
field: {
name: true,
},
title: "Cluster Name",
fieldType: FieldType.Text,
},
{
field: {
clusterIdentifier: true,
},
title: "Cluster Identifier",
fieldType: FieldType.Text,
},
{
field: {
description: true,
},
title: "Description",
fieldType: FieldType.Text,
},
{
field: {
provider: true,
},
title: "Provider",
fieldType: FieldType.Text,
},
{
field: {
lastSeenAt: true,
},
title: "Last Seen",
fieldType: FieldType.DateTime,
},
],
}}
/>
</Fragment>
);
};
export default KubernetesClusterOverview;

View File

@@ -0,0 +1,32 @@
import { getKubernetesBreadcrumbs } from "../../../Utils/Breadcrumbs";
import { RouteUtil } from "../../../Utils/RouteMap";
import PageComponentProps from "../../PageComponentProps";
import SideMenu from "./SideMenu";
import ObjectID from "Common/Types/ObjectID";
import ModelPage from "Common/UI/Components/Page/ModelPage";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import React, { FunctionComponent, ReactElement } from "react";
import { Outlet, useParams } from "react-router-dom";
const KubernetesClusterViewLayout: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const { id } = useParams();
const modelId: ObjectID = new ObjectID(id || "");
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
return (
<ModelPage
title="Kubernetes Cluster"
modelType={KubernetesCluster}
modelId={modelId}
modelNameField="name"
breadcrumbLinks={getKubernetesBreadcrumbs(path)}
sideMenu={<SideMenu modelId={modelId} />}
>
<Outlet />
</ModelPage>
);
};
export default KubernetesClusterViewLayout;

View File

@@ -0,0 +1,242 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
const KubernetesClusterNodeDetail: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
const nodeName: string = Navigation.getLastParam()?.toString() || "";
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const cpuQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_cpu",
title: "CPU Utilization",
description: `CPU utilization for node ${nodeName}`,
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.node.name": nodeName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const memoryQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_memory",
title: "Memory Usage",
description: `Memory usage for node ${nodeName}`,
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.node.name": nodeName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const filesystemQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_filesystem",
title: "Filesystem Usage",
description: `Filesystem usage for node ${nodeName}`,
legend: "Filesystem",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.filesystem.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.node.name": nodeName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const networkRxQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_network_rx",
title: "Network Receive",
description: `Network bytes received for node ${nodeName}`,
legend: "Network RX",
legendUnit: "bytes/s",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.network.io.receive",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.node.name": nodeName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const networkTxQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_network_tx",
title: "Network Transmit",
description: `Network bytes transmitted for node ${nodeName}`,
legend: "Network TX",
legendUnit: "bytes/s",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.network.io.transmit",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.node.name": nodeName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [
cpuQuery,
memoryQuery,
filesystemQuery,
networkRxQuery,
networkTxQuery,
],
formulaConfigs: [],
});
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
<Card
title={`Node Metrics: ${nodeName}`}
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
cpuQuery,
memoryQuery,
filesystemQuery,
networkRxQuery,
networkTxQuery,
],
formulaConfigs: [],
});
}}
/>
</Card>
</Fragment>
);
};
export default KubernetesClusterNodeDetail;

View File

@@ -0,0 +1,220 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
const KubernetesClusterNodes: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [metricViewData, setMetricViewData] = useState<MetricViewData | null>(
null,
);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
useEffect(() => {
if (!cluster) {
return;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const getNodeSeries: (data: AggregateModel) => ChartSeries = (
data: AggregateModel,
): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const nodeName: string =
(attributes["resource.k8s.node.name"] as string) || "Unknown Node";
return { title: nodeName };
};
const nodeCpuQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_cpu",
title: "Node CPU Utilization",
description: "CPU utilization by node",
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.cpu.utilization",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
const nodeMemoryQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_memory",
title: "Node Memory Usage",
description: "Memory usage by node",
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.memory.usage",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
const nodeFilesystemQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_filesystem",
title: "Node Filesystem Usage",
description: "Filesystem usage by node",
legend: "Filesystem",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.filesystem.usage",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
const nodeNetworkRxQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_network_rx",
title: "Node Network Receive",
description: "Network bytes received by node",
legend: "Network RX",
legendUnit: "bytes/s",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.network.io",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "receive",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
setMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [
nodeCpuQuery,
nodeMemoryQuery,
nodeFilesystemQuery,
nodeNetworkRxQuery,
],
formulaConfigs: [],
});
}, [cluster]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster || !metricViewData) {
return <ErrorMessage message="Cluster not found." />;
}
return (
<Fragment>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: metricViewData.queryConfigs,
formulaConfigs: [],
});
}}
/>
</Fragment>
);
};
export default KubernetesClusterNodes;

View File

@@ -0,0 +1,226 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
const KubernetesClusterPodDetail: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
const podName: string = Navigation.getLastParam()?.toString() || "";
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const getContainerSeries: (data: AggregateModel) => ChartSeries = (
data: AggregateModel,
): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const containerName: string =
(attributes["k8s.container.name"] as string) || "Unknown Container";
return { title: containerName };
};
const cpuQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "container_cpu",
title: "Container CPU Utilization",
description: `CPU utilization for containers in pod ${podName}`,
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "container.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.pod.name": podName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getContainerSeries,
};
const memoryQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "container_memory",
title: "Container Memory Usage",
description: `Memory usage for containers in pod ${podName}`,
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "container.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.pod.name": podName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getContainerSeries,
};
const podCpuQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_cpu",
title: "Pod CPU Utilization",
description: `CPU utilization for pod ${podName}`,
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.pod.name": podName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const podMemoryQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_memory",
title: "Pod Memory Usage",
description: `Memory usage for pod ${podName}`,
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"k8s.pod.name": podName,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery],
formulaConfigs: [],
});
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Pod Name" value={podName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
<Card
title={`Pod Metrics: ${podName}`}
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
cpuQuery,
memoryQuery,
],
formulaConfigs: [],
});
}}
/>
</Card>
</Fragment>
);
};
export default KubernetesClusterPodDetail;

View File

@@ -0,0 +1,223 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
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";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
const KubernetesClusterPods: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [metricViewData, setMetricViewData] = useState<MetricViewData | null>(
null,
);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
useEffect(() => {
if (!cluster) {
return;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const getPodSeries: (data: AggregateModel) => ChartSeries = (
data: AggregateModel,
): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const podName: string =
(attributes["resource.k8s.pod.name"] as string) || "Unknown Pod";
const namespace: string =
(attributes["resource.k8s.namespace.name"] as string) || "";
return { title: namespace ? `${namespace}/${podName}` : podName };
};
const podCpuQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_cpu",
title: "Pod CPU Utilization",
description: "CPU utilization by pod",
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.cpu.utilization",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getPodSeries,
};
const podMemoryQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_memory",
title: "Pod Memory Usage",
description: "Memory usage by pod",
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.memory.usage",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getPodSeries,
};
const podNetworkRxQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_network_rx",
title: "Pod Network Receive",
description: "Network bytes received by pod",
legend: "Network RX",
legendUnit: "bytes/s",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.network.io",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "receive",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getPodSeries,
};
const podNetworkTxQuery: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_network_tx",
title: "Pod Network Transmit",
description: "Network bytes transmitted by pod",
legend: "Network TX",
legendUnit: "bytes/s",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.network.io",
attributes: {
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "transmit",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getPodSeries,
};
setMetricViewData({
startAndEndDate: startAndEndDate,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
podNetworkRxQuery,
podNetworkTxQuery,
],
formulaConfigs: [],
});
}, [cluster]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster || !metricViewData) {
return <ErrorMessage message="Cluster not found." />;
}
return (
<Fragment>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: metricViewData.queryConfigs,
formulaConfigs: [],
});
}}
/>
</Fragment>
);
};
export default KubernetesClusterPods;

View File

@@ -0,0 +1,64 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FieldType from "Common/UI/Components/Types/FieldType";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
const KubernetesClusterSettings: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<Fragment>
<CardModelDetail<KubernetesCluster>
name="Cluster Settings"
cardProps={{
title: "Cluster Settings",
description: "Manage settings for this Kubernetes cluster.",
}}
isEditable={true}
editButtonText="Edit Settings"
modelDetailProps={{
modelType: KubernetesCluster,
id: "kubernetes-cluster-settings",
modelId: modelId,
fields: [
{
field: {
name: true,
},
title: "Name",
fieldType: FieldType.Text,
},
{
field: {
description: true,
},
title: "Description",
fieldType: FieldType.Text,
},
{
field: {
clusterIdentifier: true,
},
title: "Cluster Identifier",
fieldType: FieldType.Text,
},
{
field: {
provider: true,
},
title: "Provider",
fieldType: FieldType.Text,
},
],
}}
/>
</Fragment>
);
};
export default KubernetesClusterSettings;

View File

@@ -0,0 +1,106 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import ObjectID from "Common/Types/ObjectID";
import SideMenu from "Common/UI/Components/SideMenu/SideMenu";
import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem";
import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection";
import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps {
modelId: ObjectID;
}
const KubernetesClusterSideMenu: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<SideMenu>
<SideMenuSection title="Basic">
<SideMenuItem
link={{
title: "Overview",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Info}
/>
<SideMenuItem
link={{
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Book}
/>
</SideMenuSection>
<SideMenuSection title="Resources">
<SideMenuItem
link={{
title: "Pods",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Circle}
/>
<SideMenuItem
link={{
title: "Nodes",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Server}
/>
</SideMenuSection>
<SideMenuSection title="Observability">
<SideMenuItem
link={{
title: "Events",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Logs}
/>
<SideMenuItem
link={{
title: "Control Plane",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Activity}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">
<SideMenuItem
link={{
title: "Delete Cluster",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Trash}
className="danger-on-hover"
/>
</SideMenuSection>
</SideMenu>
);
};
export default KubernetesClusterSideMenu;

View File

@@ -1,7 +1,18 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement } from "react";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import DashboardLogsViewer from "../../Components/Logs/LogsViewer";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
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 LogsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -9,12 +20,60 @@ const LogsPage: FunctionComponent<PageComponentProps> = (
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 [showDocs, setShowDocs] = useState<boolean>(false);
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 (showDocs) {
return (
<TelemetryDocumentation
telemetryType="logs"
onClose={() => {
setShowDocs(false);
}}
/>
);
}
if (serviceCount === 0) {
return <TelemetryDocumentation telemetryType="logs" />;
}
return (
<DashboardLogsViewer
showFilters={true}
@@ -22,6 +81,9 @@ const LogsPage: FunctionComponent<PageComponentProps> = (
limit={100}
enableRealtime={true}
id="logs"
onShowDocumentation={() => {
setShowDocs(true);
}}
/>
);
};

View File

@@ -0,0 +1,11 @@
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
const MetricsDocumentationPage: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps,
): ReactElement => {
return <TelemetryDocumentation telemetryType="metrics" />;
};
export default MetricsDocumentationPage;

View File

@@ -1,7 +1,18 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import MetricsTable from "../../Components/Metrics/MetricsTable";
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 MetricsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -9,12 +20,48 @@ const MetricsPage: FunctionComponent<PageComponentProps> = (
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="metrics" />;
}
return <MetricsTable />;
};

View File

@@ -23,6 +23,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
},
],
},
{
title: "Documentation",
items: [
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.METRICS_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];
return <SideMenu sections={sections} />;

View File

@@ -0,0 +1,56 @@
import PageComponentProps from "../PageComponentProps";
import IconProp from "Common/Types/Icon/IconProp";
import Card from "Common/UI/Components/Card/Card";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
const MobileApps: FunctionComponent<PageComponentProps> = (): ReactElement => {
return (
<Fragment>
<Card
title="OneUptime On-Call - iOS"
description="Download the OneUptime On-Call app for iOS to receive push notifications and manage on-call schedules from your iPhone or iPad."
buttons={[
{
title: "Download on the App Store",
icon: IconProp.Download,
onClick: () => {
window.open(
"https://apps.apple.com/us/app/oneuptime-on-call/id6759615391",
"_blank",
);
},
},
]}
/>
<Card
title="OneUptime On-Call - Android"
description="Download the OneUptime On-Call app for Android to receive push notifications and manage on-call schedules from your Android device."
buttons={[
{
title: "Download on Google Play",
icon: IconProp.Download,
onClick: () => {
window.open(
"https://play.google.com/store/apps/details?id=com.oneuptime.oncall",
"_blank",
);
},
},
{
title: "Download APK",
icon: IconProp.Download,
onClick: () => {
window.open(
"https://github.com/OneUptime/oneuptime/releases/latest/download/oneuptime-on-call-android-app.apk",
"_blank",
);
},
},
]}
/>
</Fragment>
);
};
export default MobileApps;

View File

@@ -147,6 +147,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
},
icon: IconProp.Bell,
},
{
link: {
title: "Mobile Apps",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_MOBILE_APPS] as Route,
),
},
icon: IconProp.DevicePhoneMobile,
},
],
},
{

View File

@@ -0,0 +1,11 @@
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
const TracesDocumentationPage: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps,
): ReactElement => {
return <TelemetryDocumentation telemetryType="traces" />;
};
export default TracesDocumentationPage;

View File

@@ -1,7 +1,18 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import TraceTable from "../../Components/Traces/TraceTable";
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 TracesPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -9,12 +20,48 @@ const TracesPage: FunctionComponent<PageComponentProps> = (
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="traces" />;
}
return <TraceTable />;
};

View File

@@ -23,6 +23,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
},
],
},
{
title: "Documentation",
items: [
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.TRACES_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];
return <SideMenu sections={sections} />;

View File

@@ -20,6 +20,7 @@ export { default as StatusPagesRoutes } from "./StatusPagesRoutes";
export { default as DashboardRoutes } from "./DashboardRoutes";
export { default as ServiceRoutes } from "./ServiceRoutes";
export { default as CodeRepositoryRoutes } from "./CodeRepositoryRoutes";
export { default as KubernetesRoutes } from "./KubernetesRoutes";
export { default as AIAgentTasksRoutes } from "./AIAgentTasksRoutes";
// Settings

View File

@@ -9,11 +9,9 @@ import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import ExceptionsUnresolved from "../Pages/Exceptions/Unresolved";
import ExceptionsResolved from "../Pages/Exceptions/Resolved";
import ExceptionsArchived from "../Pages/Exceptions/Archived";
import ExceptionsDocumentationPage from "../Pages/Exceptions/Documentation";
import ExceptionView from "../Pages/Exceptions/View/Index";
const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
@@ -61,6 +59,15 @@ const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
/>
}
/>
<PageRoute
path={ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION] || ""}
element={
<ExceptionsDocumentationPage
{...props}
pageRoute={RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route}
/>
}
/>
</PageRoute>
{/* Exception View - separate from main layout */}

View File

@@ -0,0 +1,179 @@
import ComponentProps from "../Pages/PageComponentProps";
import KubernetesLayout from "../Pages/Kubernetes/Layout";
import KubernetesClusterViewLayout from "../Pages/Kubernetes/View/Layout";
import PageMap from "../Utils/PageMap";
import RouteMap, { RouteUtil, KubernetesRoutePath } from "../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import React, { FunctionComponent, ReactElement } from "react";
import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import KubernetesClusters from "../Pages/Kubernetes/Clusters";
import KubernetesClusterView from "../Pages/Kubernetes/View/Index";
import KubernetesClusterViewPods from "../Pages/Kubernetes/View/Pods";
import KubernetesClusterViewPodDetail from "../Pages/Kubernetes/View/PodDetail";
import KubernetesClusterViewNodes from "../Pages/Kubernetes/View/Nodes";
import KubernetesClusterViewNodeDetail from "../Pages/Kubernetes/View/NodeDetail";
import KubernetesClusterViewEvents from "../Pages/Kubernetes/View/Events";
import KubernetesClusterViewControlPlane from "../Pages/Kubernetes/View/ControlPlane";
import KubernetesClusterViewDelete from "../Pages/Kubernetes/View/Delete";
import KubernetesClusterViewDocumentation from "../Pages/Kubernetes/View/Documentation";
import KubernetesDocumentation from "../Pages/Kubernetes/Documentation";
const KubernetesRoutes: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<Routes>
<PageRoute path="/" element={<KubernetesLayout {...props} />}>
<PageRoute
path=""
element={
<KubernetesClusters
{...props}
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_DOCUMENTATION)}
element={
<KubernetesDocumentation
{...props}
pageRoute={RouteMap[PageMap.KUBERNETES_DOCUMENTATION] as Route}
/>
}
/>
</PageRoute>
<PageRoute
path={KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW] || ""}
element={<KubernetesClusterViewLayout {...props} />}
>
<PageRoute
index
element={
<KubernetesClusterView
{...props}
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_PODS,
)}
element={
<KubernetesClusterViewPods
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL,
)}
element={
<KubernetesClusterViewPodDetail
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_NODES,
)}
element={
<KubernetesClusterViewNodes
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL,
)}
element={
<KubernetesClusterViewNodeDetail
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS,
)}
element={
<KubernetesClusterViewEvents
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE,
)}
element={
<KubernetesClusterViewControlPlane
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_DELETE,
)}
element={
<KubernetesClusterViewDelete
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route
}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION,
)}
element={
<KubernetesClusterViewDocumentation
{...props}
pageRoute={
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION] as Route
}
/>
}
/>
</PageRoute>
</Routes>
);
};
export default KubernetesRoutes;

View File

@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import MetricsPage from "../Pages/Metrics/Index";
import MetricsDocumentationPage from "../Pages/Metrics/Documentation";
import MetricViewPage from "../Pages/Metrics/View/Index";
@@ -27,6 +28,15 @@ const MetricsRoutes: FunctionComponent<ComponentProps> = (
/>
}
/>
<PageRoute
path={MetricsRoutePath[PageMap.METRICS_DOCUMENTATION] || ""}
element={
<MetricsDocumentationPage
{...props}
pageRoute={RouteMap[PageMap.METRICS_DOCUMENTATION] as Route}
/>
}
/>
</PageRoute>
{/* Metric View */}

View File

@@ -64,6 +64,7 @@ import SettingsLogPipelineView from "../Pages/Settings/LogPipelineView";
import SettingsLogDropFilters from "../Pages/Settings/LogDropFilters";
import SettingsLogDropFilterView from "../Pages/Settings/LogDropFilterView";
import SettingsLogScrubRules from "../Pages/Settings/LogScrubRules";
import SettingsMobileApps from "../Pages/Settings/MobileApps";
const SettingsRoutes: FunctionComponent<ComponentProps> = (
props: ComponentProps,
@@ -92,6 +93,15 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_MOBILE_APPS)}
element={
<SettingsMobileApps
{...props}
pageRoute={RouteMap[PageMap.SETTINGS_MOBILE_APPS] as Route}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_AI_LOGS)}
element={

View File

@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import TracesPage from "../Pages/Traces/Index";
import TracesDocumentationPage from "../Pages/Traces/Documentation";
import TraceViewPage from "../Pages/Traces/View/Index";
@@ -27,6 +28,15 @@ const TracesRoutes: FunctionComponent<ComponentProps> = (
/>
}
/>
<PageRoute
path={TracesRoutePath[PageMap.TRACES_DOCUMENTATION] || ""}
element={
<TracesDocumentationPage
{...props}
pageRoute={RouteMap[PageMap.TRACES_DOCUMENTATION] as Route}
/>
}
/>
</PageRoute>
{/* Trace View */}

View File

@@ -31,6 +31,11 @@ export function getExceptionsBreadcrumbs(
"Exceptions",
"View Exception",
]),
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_DOCUMENTATION, [
"Project",
"Exceptions",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -0,0 +1,67 @@
import PageMap from "../PageMap";
import { BuildBreadcrumbLinksByTitles } from "./Helper";
import Dictionary from "Common/Types/Dictionary";
import Link from "Common/Types/Link";
export function getKubernetesBreadcrumbs(
path: string,
): Array<Link> | undefined {
const breadcrumpLinksMap: Dictionary<Link[]> = {
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTERS, [
"Project",
"Kubernetes",
"Clusters",
]),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW, [
"Project",
"Kubernetes",
"View Cluster",
]),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_PODS, [
"Project",
"Kubernetes",
"View Cluster",
"Pods",
]),
...BuildBreadcrumbLinksByTitles(
PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL,
["Project", "Kubernetes", "View Cluster", "Pods", "Pod Detail"],
),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_NODES, [
"Project",
"Kubernetes",
"View Cluster",
"Nodes",
]),
...BuildBreadcrumbLinksByTitles(
PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL,
["Project", "Kubernetes", "View Cluster", "Nodes", "Node Detail"],
),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS, [
"Project",
"Kubernetes",
"View Cluster",
"Events",
]),
...BuildBreadcrumbLinksByTitles(
PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE,
["Project", "Kubernetes", "View Cluster", "Control Plane"],
),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_DELETE, [
"Project",
"Kubernetes",
"View Cluster",
"Delete Cluster",
]),
...BuildBreadcrumbLinksByTitles(
PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION,
["Project", "Kubernetes", "View Cluster", "Documentation"],
),
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_DOCUMENTATION, [
"Project",
"Kubernetes",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -11,6 +11,11 @@ export function getMetricsBreadcrumbs(path: string): Array<Link> | undefined {
"Metrics",
"Metrics Explorer",
]),
...BuildBreadcrumbLinksByTitles(PageMap.METRICS_DOCUMENTATION, [
"Project",
"Metrics",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -11,6 +11,11 @@ export function getTracesBreadcrumbs(path: string): Array<Link> | undefined {
"Traces",
"Trace Explorer",
]),
...BuildBreadcrumbLinksByTitles(PageMap.TRACES_DOCUMENTATION, [
"Project",
"Traces",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -11,6 +11,7 @@ export * from "./SettingsBreadcrumbs";
export * from "./MonitorGroupBreadcrumbs";
export * from "./ServiceBreadcrumbs";
export * from "./CodeRepositoryBreadcrumbs";
export * from "./KubernetesBreadcrumbs";
export * from "./DashboardBreadCrumbs";
export * from "./AIAgentTasksBreadcrumbs";
export * from "./ExceptionsBreadcrumbs";

View File

@@ -16,11 +16,13 @@ enum PageMap {
METRICS_ROOT = "METRICS_ROOT",
METRICS = "METRICS",
METRIC_VIEW = "METRIC_VIEW",
METRICS_DOCUMENTATION = "METRICS_DOCUMENTATION",
// Traces (standalone product)
TRACES_ROOT = "TRACES_ROOT",
TRACES = "TRACES",
TRACE_VIEW = "TRACE_VIEW",
TRACES_DOCUMENTATION = "TRACES_DOCUMENTATION",
HOME = "HOME",
HOME_NOT_OPERATIONAL_MONITORS = "HOME_NOT_OPERATIONAL_MONITORS",
@@ -214,6 +216,21 @@ enum PageMap {
SERVICE_VIEW_CODE_REPOSITORIES = "SERVICE_VIEW_CODE_REPOSITORIES",
SERVICE_DEPENDENCY_GRAPH = "SERVICE_DEPENDENCY_GRAPH",
// Kubernetes (standalone product)
KUBERNETES_ROOT = "KUBERNETES_ROOT",
KUBERNETES_CLUSTERS = "KUBERNETES_CLUSTERS",
KUBERNETES_CLUSTER_VIEW = "KUBERNETES_CLUSTER_VIEW",
KUBERNETES_CLUSTER_VIEW_PODS = "KUBERNETES_CLUSTER_VIEW_PODS",
KUBERNETES_CLUSTER_VIEW_POD_DETAIL = "KUBERNETES_CLUSTER_VIEW_POD_DETAIL",
KUBERNETES_CLUSTER_VIEW_NODES = "KUBERNETES_CLUSTER_VIEW_NODES",
KUBERNETES_CLUSTER_VIEW_NODE_DETAIL = "KUBERNETES_CLUSTER_VIEW_NODE_DETAIL",
KUBERNETES_CLUSTER_VIEW_EVENTS = "KUBERNETES_CLUSTER_VIEW_EVENTS",
KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE = "KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE",
KUBERNETES_CLUSTER_VIEW_DELETE = "KUBERNETES_CLUSTER_VIEW_DELETE",
KUBERNETES_CLUSTER_VIEW_SETTINGS = "KUBERNETES_CLUSTER_VIEW_SETTINGS",
KUBERNETES_CLUSTER_VIEW_DOCUMENTATION = "KUBERNETES_CLUSTER_VIEW_DOCUMENTATION",
KUBERNETES_DOCUMENTATION = "KUBERNETES_DOCUMENTATION",
// Code Repository
CODE_REPOSITORY_ROOT = "CODE_REPOSITORY_ROOT",
CODE_REPOSITORY = "CODE_REPOSITORY",
@@ -432,6 +449,9 @@ enum PageMap {
SETTINGS_NOTIFICATION_LOGS = "SETTINGS_NOTIFICATION_LOGS",
// Mobile Apps
SETTINGS_MOBILE_APPS = "SETTINGS_MOBILE_APPS",
// AI Logs
SETTINGS_AI_LOGS = "SETTINGS_AI_LOGS",
@@ -451,6 +471,7 @@ enum PageMap {
EXCEPTIONS_ARCHIVED = "EXCEPTIONS_ARCHIVED",
EXCEPTIONS_VIEW_ROOT = "EXCEPTIONS_VIEW_ROOT",
EXCEPTIONS_VIEW = "EXCEPTIONS_VIEW",
EXCEPTIONS_DOCUMENTATION = "EXCEPTIONS_DOCUMENTATION",
// Push Logs in resource views
}

View File

@@ -59,6 +59,20 @@ export const CodeRepositoryRoutePath: Dictionary<string> = {
[PageMap.CODE_REPOSITORY_VIEW_SERVICES]: `${RouteParams.ModelID}/services`,
};
export const KubernetesRoutePath: Dictionary<string> = {
[PageMap.KUBERNETES_CLUSTER_VIEW]: `${RouteParams.ModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: `${RouteParams.ModelID}/pods`,
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: `${RouteParams.ModelID}/pods/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: `${RouteParams.ModelID}/nodes`,
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: `${RouteParams.ModelID}/nodes/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: `${RouteParams.ModelID}/events`,
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: `${RouteParams.ModelID}/control-plane`,
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]: `${RouteParams.ModelID}/documentation`,
[PageMap.KUBERNETES_DOCUMENTATION]: `documentation`,
};
export const WorkflowRoutePath: Dictionary<string> = {
[PageMap.WORKFLOWS_LOGS]: "logs",
[PageMap.WORKFLOWS_VARIABLES]: "variables",
@@ -86,12 +100,14 @@ export const LogsRoutePath: Dictionary<string> = {
export const MetricsRoutePath: Dictionary<string> = {
[PageMap.METRICS]: "",
[PageMap.METRIC_VIEW]: "view",
[PageMap.METRICS_DOCUMENTATION]: "documentation",
};
// Traces product routes
export const TracesRoutePath: Dictionary<string> = {
[PageMap.TRACES]: "",
[PageMap.TRACE_VIEW]: `view/${RouteParams.ModelID}`,
[PageMap.TRACES_DOCUMENTATION]: "documentation",
};
export const ExceptionsRoutePath: Dictionary<string> = {
@@ -101,6 +117,7 @@ export const ExceptionsRoutePath: Dictionary<string> = {
[PageMap.EXCEPTIONS_ARCHIVED]: "archived",
[PageMap.EXCEPTIONS_VIEW_ROOT]: "",
[PageMap.EXCEPTIONS_VIEW]: `${RouteParams.ModelID}`,
[PageMap.EXCEPTIONS_DOCUMENTATION]: "documentation",
};
export const DashboardsRoutePath: Dictionary<string> = {
@@ -299,6 +316,7 @@ export const SettingsRoutePath: Dictionary<string> = {
[PageMap.SETTINGS_DANGERZONE]: "danger-zone",
[PageMap.SETTINGS_NOTIFICATION_SETTINGS]: "notification-settings",
[PageMap.SETTINGS_NOTIFICATION_LOGS]: "notification-logs",
[PageMap.SETTINGS_MOBILE_APPS]: "mobile-apps",
[PageMap.SETTINGS_AI_LOGS]: "ai-logs",
[PageMap.SETTINGS_APIKEYS]: `api-keys`,
[PageMap.SETTINGS_APIKEY_VIEW]: `api-keys/${RouteParams.ModelID}`,
@@ -1465,6 +1483,82 @@ const RouteMap: Dictionary<Route> = {
}`,
),
// Kubernetes
[PageMap.KUBERNETES_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/*`,
),
[PageMap.KUBERNETES_CLUSTERS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]
}`,
),
[PageMap.KUBERNETES_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_DOCUMENTATION]
}`,
),
// Dashboards
[PageMap.DASHBOARDS_ROOT]: new Route(
@@ -1981,6 +2075,12 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.METRICS_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/metrics/${
MetricsRoutePath[PageMap.METRICS_DOCUMENTATION]
}`,
),
// Traces Product Routes
[PageMap.TRACES_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/traces/*`,
@@ -1994,6 +2094,12 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.TRACES_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/traces/${
TracesRoutePath[PageMap.TRACES_DOCUMENTATION]
}`,
),
// User Settings Routes
[PageMap.USER_SETTINGS_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/user-settings/*`,
@@ -2105,6 +2211,12 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.SETTINGS_MOBILE_APPS]: new Route(
`/dashboard/${RouteParams.ProjectID}/settings/${
SettingsRoutePath[PageMap.SETTINGS_MOBILE_APPS]
}`,
),
[PageMap.SETTINGS_AI_LOGS]: new Route(
`/dashboard/${RouteParams.ProjectID}/settings/${
SettingsRoutePath[PageMap.SETTINGS_AI_LOGS]
@@ -2468,6 +2580,12 @@ const RouteMap: Dictionary<Route> = {
ExceptionsRoutePath[PageMap.EXCEPTIONS_VIEW]
}`,
),
[PageMap.EXCEPTIONS_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/exceptions/${
ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION]
}`,
),
};
export class RouteUtil {

View File

@@ -1,5 +1,6 @@
import AcmeCertificate from "./AcmeCertificate";
import AcmeChallenge from "./AcmeChallenge";
import KubernetesCluster from "./KubernetesCluster";
// API Keys
import ApiKey from "./ApiKey";
import ApiKeyPermission from "./ApiKeyPermission";
@@ -499,6 +500,8 @@ const AllModelTypes: Array<{
ProjectSCIM,
ProjectSCIMLog,
StatusPageSCIMLog,
KubernetesCluster,
];
const modelTypeMap: { [key: string]: { new (): BaseModel } } = {};

View File

@@ -0,0 +1,640 @@
import Label from "./Label";
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import AccessControlColumn from "../../Types/Database/AccessControlColumn";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import UniqueColumnBy from "../../Types/Database/UniqueColumnBy";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@AccessControlColumn("labels")
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteKubernetesCluster,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@CrudApiEndpoint(new Route("/kubernetes-cluster"))
@SlugifyColumn("name", "slug")
@TableMetadata({
tableName: "KubernetesCluster",
singularName: "Kubernetes Cluster",
pluralName: "Kubernetes Clusters",
icon: IconProp.Cube,
tableDescription:
"Kubernetes Clusters that are being monitored in this project. Each cluster is auto-discovered when the OneUptime kubernetes-agent sends metrics, or can be manually registered.",
})
@Entity({
name: "KubernetesCluster",
})
export default class KubernetesCluster extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Name",
description: "Friendly name for this Kubernetes cluster",
example: "production-us-east",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
@UniqueColumnBy("projectId")
public name?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
required: true,
unique: true,
type: TableColumnType.Slug,
computed: true,
title: "Slug",
description: "Friendly globally unique name for your object",
})
@Column({
nullable: false,
type: ColumnType.Slug,
length: ColumnLength.Slug,
})
public slug?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
title: "Description",
description: "Friendly description for this Kubernetes cluster",
example: "Production cluster running in US East region on EKS",
})
@Column({
nullable: true,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public description?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Cluster Identifier",
description:
"Unique identifier for this cluster, sourced from the k8s.cluster.name OTel resource attribute",
example: "production-us-east-1",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
@UniqueColumnBy("projectId")
public clusterIdentifier?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Provider",
description:
"Cloud provider or platform running this cluster (EKS, GKE, AKS, self-managed, unknown)",
example: "EKS",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
default: "unknown",
})
public provider?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "OTel Collector Status",
description:
"Connection status of the OTel Collector agent (connected or disconnected)",
example: "connected",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
default: "disconnected",
})
public otelCollectorStatus?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: false,
type: TableColumnType.Date,
canReadOnRelationQuery: true,
title: "Last Seen At",
description: "When metrics were last received from this cluster",
})
@Column({
nullable: true,
type: ColumnType.Date,
})
public lastSeenAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
type: TableColumnType.Number,
title: "Node Count",
description: "Cached count of nodes in this cluster",
})
@Column({
type: ColumnType.Number,
nullable: true,
default: 0,
})
public nodeCount?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
type: TableColumnType.Number,
title: "Pod Count",
description: "Cached count of pods in this cluster",
})
@Column({
type: ColumnType.Number,
nullable: true,
default: 0,
})
public podCount?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
type: TableColumnType.Number,
title: "Namespace Count",
description: "Cached count of namespaces in this cluster",
})
@Column({
type: ColumnType.Number,
nullable: true,
default: 0,
})
public namespaceCount?: number = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateKubernetesCluster,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadKubernetesCluster,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditKubernetesCluster,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Label,
title: "Labels",
description:
"Relation to Labels Array where this object is categorized in.",
})
@ManyToMany(
() => {
return Label;
},
{ eager: false },
)
@JoinTable({
name: "KubernetesClusterLabel",
inverseJoinColumn: {
name: "labelId",
referencedColumnName: "_id",
},
joinColumn: {
name: "kubernetesClusterId",
referencedColumnName: "_id",
},
})
public labels?: Array<Label> = undefined;
}

View File

@@ -0,0 +1,137 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1773761409952 implements MigrationInterface {
public name = "MigrationName1773761409952";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`CREATE INDEX "IDX_5ae5bbb0c93c048b0b76b1d426" ON "KubernetesCluster" ("projectId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_b9259f6741a7965a518e258f61" ON "KubernetesCluster" ("clusterIdentifier") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_ed1b53bd041aa21b44ca8cdab5" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_2ec82ad068e84cf762c32ad7c7" ON "KubernetesClusterLabel" ("labelId") `,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_1bee392c44b1aebe754932133a8" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_1bee392c44b1aebe754932133a8"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_2ec82ad068e84cf762c32ad7c7"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_ed1b53bd041aa21b44ca8cdab5"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_b9259f6741a7965a518e258f61"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_5ae5bbb0c93c048b0b76b1d426"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId") `,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}

View File

@@ -0,0 +1,80 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1774000000000 implements MigrationInterface {
public name = "MigrationName1774000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "KubernetesCluster" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "slug" character varying(100) NOT NULL, "description" character varying(500), "clusterIdentifier" character varying(100) NOT NULL, "provider" character varying(100) DEFAULT 'unknown', "otelCollectorStatus" character varying(100) DEFAULT 'disconnected', "lastSeenAt" TIMESTAMP WITH TIME ZONE, "nodeCount" integer DEFAULT '0', "podCount" integer DEFAULT '0', "namespaceCount" integer DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_kubernetes_cluster_id" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier")`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug")`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
// Label join table
await queryRunner.query(
`CREATE TABLE "KubernetesClusterLabel" ("kubernetesClusterId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_kubernetes_cluster_label" PRIMARY KEY ("kubernetesClusterId", "labelId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId")`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
);
await queryRunner.query(`DROP TABLE "KubernetesClusterLabel"`);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
);
await queryRunner.query(
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
);
await queryRunner.query(`DROP TABLE "KubernetesCluster"`);
}
}

View File

@@ -266,6 +266,8 @@ import { AddLogSavedView1772355000000 } from "./1772355000000-AddLogSavedView";
import { MigrationName1773344537755 } from "./1773344537755-MigrationName";
import { MigrationName1773402621107 } from "./1773402621107-MigrationName";
import { MigrationName1773676206197 } from "./1773676206197-MigrationName";
import { MigrationName1774000000000 } from "./1774000000000-MigrationName";
import { MigrationName1773761409952 } from "./1773761409952-MigrationName";
export default [
InitialMigration,
@@ -536,4 +538,6 @@ export default [
MigrationName1773344537755,
MigrationName1773402621107,
MigrationName1773676206197,
MigrationName1774000000000,
MigrationName1773761409952,
];

View File

@@ -0,0 +1,109 @@
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/KubernetesCluster";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import ObjectID from "../../Types/ObjectID";
import QueryHelper from "../Types/Database/QueryHelper";
import OneUptimeDate from "../../Types/Date";
import LIMIT_MAX from "../../Types/Database/LimitMax";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
public async findOrCreateByClusterIdentifier(data: {
projectId: ObjectID;
clusterIdentifier: string;
}): Promise<Model> {
// Try to find existing cluster
const existingCluster: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
clusterIdentifier: data.clusterIdentifier,
},
select: {
_id: true,
projectId: true,
clusterIdentifier: true,
},
props: {
isRoot: true,
},
});
if (existingCluster) {
return existingCluster;
}
// Create new cluster
const newCluster: Model = new Model();
newCluster.projectId = data.projectId;
newCluster.name = data.clusterIdentifier;
newCluster.clusterIdentifier = data.clusterIdentifier;
newCluster.otelCollectorStatus = "connected";
newCluster.lastSeenAt = OneUptimeDate.getCurrentDate();
const createdCluster: Model = await this.create({
data: newCluster,
props: {
isRoot: true,
},
});
return createdCluster;
}
@CaptureSpan()
public async updateLastSeen(clusterId: ObjectID): Promise<void> {
await this.updateOneById({
id: clusterId,
data: {
lastSeenAt: OneUptimeDate.getCurrentDate(),
otelCollectorStatus: "connected",
},
props: {
isRoot: true,
},
});
}
@CaptureSpan()
public async markDisconnectedClusters(): Promise<void> {
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
OneUptimeDate.getCurrentDate(),
-5,
);
const connectedClusters: Array<Model> = await this.findBy({
query: {
otelCollectorStatus: "connected",
lastSeenAt: QueryHelper.lessThan(fiveMinutesAgo),
},
select: {
_id: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const cluster of connectedClusters) {
if (cluster._id) {
await this.updateOneById({
id: new ObjectID(cluster._id.toString()),
data: {
otelCollectorStatus: "disconnected",
},
props: {
isRoot: true,
},
});
}
}
}
}
export default new Service();

View File

@@ -37,6 +37,7 @@ import Typeof from "../../Types/Typeof";
import CookieParser from "cookie-parser";
import cors from "cors";
import zlib from "zlib";
import path from "path";
import "ejs";
// Make sure we have stack trace for debugging.
Error.stackTraceLimit = Infinity;
@@ -240,12 +241,15 @@ const init: InitFunction = async (
},
);
app.use(`/${appName}`, ExpressStatic("/usr/src/app/public"));
app.use(
`/${appName}`,
ExpressStatic(path.resolve(process.cwd(), "public")),
);
app.get(
`/${appName}/dist/Index.js`,
(_req: ExpressRequest, res: ExpressResponse) => {
res.sendFile("/usr/src/app/public/dist/Index.js");
res.sendFile(path.resolve(process.cwd(), "public/dist/Index.js"));
},
);
@@ -285,7 +289,7 @@ const init: InitFunction = async (
return;
}
return res.render("/usr/src/app/views/index.ejs", {
return res.render(path.resolve(process.cwd(), "views/index.ejs"), {
enableGoogleTagManager: IsBillingEnabled || false,
...variables,
});

View File

@@ -320,6 +320,7 @@ enum IconProp {
TableCellsIcon = "TableCellsIcon",
UserIcon = "UserIcon",
XCircle = "XCircle",
Kubernetes = "Kubernetes",
}
export default IconProp;

View File

@@ -705,6 +705,11 @@ enum Permission {
DeleteAlertSeverity = "DeleteAlertSeverity",
ReadAlertSeverity = "ReadAlertSeverity",
CreateKubernetesCluster = "CreateKubernetesCluster",
DeleteKubernetesCluster = "DeleteKubernetesCluster",
EditKubernetesCluster = "EditKubernetesCluster",
ReadKubernetesCluster = "ReadKubernetesCluster",
CreateService = "CreateService",
DeleteService = "DeleteService",
EditService = "EditService",
@@ -4324,6 +4329,43 @@ export class PermissionHelper {
group: PermissionGroup.AIAgent,
},
{
permission: Permission.CreateKubernetesCluster,
title: "Create Kubernetes Cluster",
description:
"This permission can create Kubernetes Cluster in this project.",
isAssignableToTenant: true,
isAccessControlPermission: true,
group: PermissionGroup.Telemetry,
},
{
permission: Permission.DeleteKubernetesCluster,
title: "Delete Kubernetes Cluster",
description:
"This permission can delete Kubernetes Cluster of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Telemetry,
},
{
permission: Permission.EditKubernetesCluster,
title: "Edit Kubernetes Cluster",
description:
"This permission can edit Kubernetes Cluster of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Telemetry,
},
{
permission: Permission.ReadKubernetesCluster,
title: "Read Kubernetes Cluster",
description:
"This permission can read Kubernetes Cluster of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
group: PermissionGroup.Telemetry,
},
{
permission: Permission.CreateService,
title: "Create Service",

View File

@@ -2733,6 +2733,57 @@ const Icon: FunctionComponent<ComponentProps> = ({
d="m15 11.25 1.5 1.5.75-.75V8.758l2.276-.61a3 3 0 1 0-3.675-3.675l-.61 2.277H12l-.75.75 1.5 1.5M15 11.25l-8.47 8.47c-.34.34-.8.53-1.28.53s-.94-.19-1.28-.53a1.818 1.818 0 0 1 0-2.56l8.47-8.47M15 11.25 12 8.25"
/>,
);
} else if (icon === IconProp.Kubernetes) {
// Kubernetes helm wheel — 7-sided shape with 7 spokes, matching the official logo
const cx: number = 12;
const cy: number = 12;
const outerR: number = 9.5;
const innerR: number = 2.2;
const spokeEnd: number = 8;
const sides: number = 7;
const offsetAngle: number = -Math.PI / 2; // start from top
const outerPoints: string[] = [];
const spokes: React.ReactElement[] = [];
for (let i: number = 0; i < sides; i++) {
const angle: number = offsetAngle + (2 * Math.PI * i) / sides;
const ox: number = cx + outerR * Math.cos(angle);
const oy: number = cy + outerR * Math.sin(angle);
outerPoints.push(`${ox.toFixed(2)},${oy.toFixed(2)}`);
const sx: number = cx + innerR * Math.cos(angle);
const sy: number = cy + innerR * Math.sin(angle);
const ex: number = cx + spokeEnd * Math.cos(angle);
const ey: number = cy + spokeEnd * Math.sin(angle);
spokes.push(
<line
key={`spoke-${i}`}
x1={sx.toFixed(2)}
y1={sy.toFixed(2)}
x2={ex.toFixed(2)}
y2={ey.toFixed(2)}
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>,
);
}
return getSvgWrapper(
<>
<polygon
points={outerPoints.join(" ")}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<circle cx={cx} cy={cy} r={innerR} />
{spokes}
</>,
);
}
return <></>;

View File

@@ -102,6 +102,7 @@ export interface ComponentProps {
onEditSavedView?: ((viewId: string) => void) | undefined;
onDeleteSavedView?: ((viewId: string) => void) | undefined;
onUpdateCurrentSavedView?: (() => void) | undefined;
onShowDocumentation?: (() => void) | undefined;
viewMode?: LogsViewMode | undefined;
onViewModeChange?: ((mode: LogsViewMode) => void) | undefined;
analyticsServiceIds?: Array<string> | undefined;
@@ -805,6 +806,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
return !prev;
});
},
onShowDocumentation: props.onShowDocumentation,
};
const showSidebar: boolean =

View File

@@ -37,6 +37,7 @@ export interface LogsViewerToolbarProps {
onExportJSON?: (() => void) | undefined;
showKeyboardShortcuts?: boolean | undefined;
onToggleKeyboardShortcuts?: (() => void) | undefined;
onShowDocumentation?: (() => void) | undefined;
}
const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
@@ -155,6 +156,30 @@ const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
/>
)}
{props.onShowDocumentation && (
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50"
onClick={props.onShowDocumentation}
title="Setup Documentation"
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
/>
</svg>
Docs
</button>
)}
{props.onToggleKeyboardShortcuts && (
<div className="relative">
<button

View File

@@ -208,9 +208,9 @@ const CodeBlock: FunctionComponent<{
(language ? language.charAt(0).toUpperCase() + language.slice(1) : "");
return (
<div className="relative rounded-lg mt-4 mb-4 overflow-hidden border border-gray-700">
<div className="relative rounded-lg mt-3 mb-3 overflow-hidden border border-gray-200 shadow-sm">
{/* Header bar */}
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-800/60 border-b border-gray-700/60">
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-800 border-b border-gray-700">
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-400 select-none">
{displayLang}
</span>
@@ -261,7 +261,7 @@ const CodeBlock: FunctionComponent<{
children={content}
language={language}
style={vscDarkPlus}
className="!rounded-none !mt-0 !mb-0 !bg-gray-900 !pt-6 !pb-4 !px-4 text-sm !border-0"
className="!rounded-none !mt-0 !mb-0 !bg-gray-900 !pt-3 !pb-3 !px-4 text-sm !border-0"
codeTagProps={{ className: "font-mono" }}
/>
</div>
@@ -279,7 +279,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h1: ({ ...props }: any) => {
return (
<h1
className="text-4xl mt-8 mb-6 border-b-2 border-blue-500 pb-2 text-gray-900 font-bold"
className="text-lg mt-6 mb-3 text-gray-900 font-bold"
{...props}
/>
);
@@ -287,7 +287,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h2: ({ ...props }: any) => {
return (
<h2
className="text-3xl mt-6 mb-4 border-b border-gray-300 pb-1 text-gray-900 font-semibold"
className="text-base mt-6 mb-2 text-gray-900 font-semibold"
{...props}
/>
);
@@ -295,7 +295,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h3: ({ ...props }: any) => {
return (
<h3
className="text-2xl mt-6 mb-3 text-gray-900 font-semibold"
className="text-base mt-4 mb-2 text-gray-900 font-semibold"
{...props}
/>
);
@@ -303,7 +303,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h4: ({ ...props }: any) => {
return (
<h4
className="text-xl mt-5 mb-3 text-gray-900 font-medium"
className="text-sm mt-3 mb-2 text-gray-900 font-semibold"
{...props}
/>
);
@@ -311,7 +311,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h5: ({ ...props }: any) => {
return (
<h5
className="text-lg mt-4 mb-2 text-gray-900 font-medium"
className="text-sm mt-3 mb-1 text-gray-900 font-medium"
{...props}
/>
);
@@ -319,7 +319,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
h6: ({ ...props }: any) => {
return (
<h6
className="text-base mt-3 mb-2 text-gray-900 font-medium"
className="text-sm mt-2 mb-1 text-gray-700 font-medium"
{...props}
/>
);
@@ -327,7 +327,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
p: ({ ...props }: any) => {
return (
<p
className="text-base mt-3 mb-4 text-gray-700 leading-relaxed"
className="text-sm mt-2 mb-3 text-gray-700 leading-relaxed"
{...props}
/>
);
@@ -352,24 +352,24 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
return <>{children}</>;
}
// Avoid double borders when SyntaxHighlighter is already styling the block.
const isSyntaxHighlighter: boolean =
/*
* If the child is a custom component (CodeBlock, MermaidDiagram, etc.)
* rather than a plain HTML element like <code>, skip pre styling.
* Checking typeof type !== "string" is minification-safe unlike checking type.name.
*/
const isCustomComponent: boolean =
React.isValidElement(children) &&
// name can be 'SyntaxHighlighter' or wrapped/minified; fall back to presence of 'children' prop with 'react-syntax-highlighter' data attribute.
(((children as any).type &&
((children as any).type.name === "SyntaxHighlighter" ||
(children as any).type.displayName ===
"SyntaxHighlighter")) ||
(children as any).props?.className?.includes(
"syntax-highlighter",
));
typeof (children as any).type !== "string";
const baseClass: string = isSyntaxHighlighter
? "mt-4 mb-4 rounded-lg overflow-hidden"
: "bg-gray-900 text-gray-100 mt-4 mb-4 p-2 rounded-lg text-sm overflow-x-auto border border-gray-700";
if (isCustomComponent) {
return <>{children}</>;
}
return (
<pre className={baseClass} {...rest}>
<pre
className="bg-gray-900 text-gray-100 mt-3 mb-3 p-3 rounded-md text-sm overflow-x-auto border border-gray-700"
{...rest}
>
{children}
</pre>
);
@@ -377,7 +377,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
strong: ({ ...props }: any) => {
return (
<strong
className="text-base font-semibold text-gray-900"
className="text-sm font-semibold text-gray-900"
{...props}
/>
);
@@ -385,56 +385,76 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
li: ({ ...props }: any) => {
return (
<li
className="text-base mt-2 mb-1 text-gray-700 leading-relaxed"
className="text-sm mt-1 mb-1 text-gray-700 leading-relaxed"
{...props}
/>
);
},
ul: ({ ...props }: any) => {
return <ul className="list-disc pl-8 mt-2 mb-4" {...props} />;
return <ul className="list-disc pl-6 mt-1 mb-3" {...props} />;
},
ol: ({ ...props }: any) => {
return <ol className="list-decimal pl-8 mt-2 mb-4" {...props} />;
return <ol className="list-decimal pl-6 mt-1 mb-3" {...props} />;
},
blockquote: ({ ...props }: any) => {
blockquote: ({ children, ...props }: any) => {
return (
<blockquote
className="border-l-4 border-blue-500 pl-4 italic text-gray-600 bg-gray-50 py-2 my-4"
className="rounded-lg border border-amber-200 bg-amber-50/50 my-4 not-italic overflow-hidden"
{...props}
/>
>
<div className="flex items-start gap-3 px-4 py-3">
<svg
className="h-5 w-5 flex-shrink-0 text-amber-500 mt-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<div className="text-sm text-gray-700 leading-relaxed [&>p]:mt-0 [&>p]:mb-0 [&>p>strong:first-child]:text-amber-700 [&>p>strong:first-child]:mr-1">
{children}
</div>
</div>
</blockquote>
);
},
table: ({ ...props }: any) => {
return (
<table
className="min-w-full table-auto border-collapse border border-gray-300 mt-4 mb-4"
{...props}
/>
<div className="overflow-hidden rounded-lg border border-gray-200 mt-4 mb-4 shadow-sm">
<table
className="min-w-full table-auto border-collapse text-sm"
{...props}
/>
</div>
);
},
thead: ({ ...props }: any) => {
return <thead className="bg-gray-100" {...props} />;
return <thead className="bg-gray-50" {...props} />;
},
tbody: ({ ...props }: any) => {
return <tbody {...props} />;
return <tbody className="divide-y divide-gray-100" {...props} />;
},
tr: ({ ...props }: any) => {
return <tr className="border-b border-gray-200" {...props} />;
return (
<tr className="hover:bg-gray-50 transition-colors" {...props} />
);
},
th: ({ ...props }: any) => {
return (
<th
className="px-4 py-2 text-left text-sm font-semibold text-gray-900 border border-gray-300"
className="px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 border-b border-gray-200"
{...props}
/>
);
},
td: ({ ...props }: any) => {
return (
<td
className="px-4 py-2 text-sm text-gray-700 border border-gray-300"
{...props}
/>
<td className="px-4 py-2.5 text-sm text-gray-700" {...props} />
);
},
hr: ({ ...props }: any) => {
@@ -457,19 +477,31 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
return <MermaidDiagram chart={content} />;
}
const codeClassName: string =
content.includes("\n") ||
(match &&
const isMultiline: boolean = content.includes("\n");
const hasLanguage: boolean = Boolean(
match &&
match?.filter((item: string) => {
return item.includes("language-");
}).length > 0)
? ""
: "text-sm px-2 py-1 bg-gray-200 rounded text-gray-900 font-mono";
}).length > 0,
);
return match ? (
<CodeBlock language={match[1]!} content={content} rest={rest} />
) : (
<code className={codeClassName} {...rest}>
// Multiline code blocks (with or without language) get the full CodeBlock treatment
if (hasLanguage || isMultiline) {
return (
<CodeBlock
language={match ? match[1]! : "text"}
content={content}
rest={rest}
/>
);
}
// Inline code
return (
<code
className="text-xs px-1.5 py-0.5 bg-gray-100 border border-gray-200 rounded text-gray-800 font-mono"
{...rest}
>
{children}
</code>
);

View File

@@ -39,7 +39,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
);
return (
<div className="absolute left-0 z-10 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
<div className="absolute left-0 z-50 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
{/* Sections */}
<div className="p-6">
@@ -127,7 +127,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
return (
<div
className={`absolute left-1/2 z-10 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
className={`absolute left-1/2 z-50 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
>
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
{/* Menu Items */}

View File

@@ -1,5 +1,7 @@
// Critical Path Analysis for distributed traces
// Computes self-time, critical path, and bottleneck identification
/*
* Critical Path Analysis for distributed traces
* Computes self-time, critical path, and bottleneck identification
*/
export interface SpanData {
spanId: string;
@@ -71,9 +73,10 @@ export default class CriticalPathUtil {
selfTimeUnixNano,
childTimeUnixNano,
totalTimeUnixNano: span.durationUnixNano,
selfTimePercent: span.durationUnixNano > 0
? (selfTimeUnixNano / span.durationUnixNano) * 100
: 0,
selfTimePercent:
span.durationUnixNano > 0
? (selfTimeUnixNano / span.durationUnixNano) * 100
: 0,
});
}
@@ -110,9 +113,14 @@ export default class CriticalPathUtil {
}
// Sort by start time
intervals.sort((a: { start: number; end: number }, b: { start: number; end: number }) => {
return a.start - b.start;
});
intervals.sort(
(
a: { start: number; end: number },
b: { start: number; end: number },
) => {
return a.start - b.start;
},
);
// Merge overlapping intervals
let mergedDuration: number = 0;
@@ -198,9 +206,10 @@ export default class CriticalPathUtil {
const criticalPathCache: Map<string, { weight: number; path: string[] }> =
new Map();
const computeWeight = (
spanId: string,
): { weight: number; path: string[] } => {
const computeWeight: (spanId: string) => {
weight: number;
path: string[];
} = (spanId: string): { weight: number; path: string[] } => {
const cached: { weight: number; path: string[] } | undefined =
criticalPathCache.get(spanId);
if (cached) {
@@ -275,9 +284,7 @@ export default class CriticalPathUtil {
/**
* Compute latency breakdown by service.
*/
public static computeServiceBreakdown(
spans: SpanData[],
): ServiceBreakdown[] {
public static computeServiceBreakdown(spans: SpanData[]): ServiceBreakdown[] {
const selfTimes: Map<string, SpanSelfTime> =
CriticalPathUtil.computeSelfTimes(spans);
@@ -302,12 +309,15 @@ export default class CriticalPathUtil {
for (const span of spans) {
const serviceId: string = span.serviceId || "unknown";
const entry: { totalDuration: number; selfTime: number; spanCount: number } =
serviceMap.get(serviceId) || {
totalDuration: 0,
selfTime: 0,
spanCount: 0,
};
const entry: {
totalDuration: number;
selfTime: number;
spanCount: number;
} = serviceMap.get(serviceId) || {
totalDuration: 0,
selfTime: 0,
spanCount: 0,
};
entry.totalDuration += span.durationUnixNano;
const selfTime: SpanSelfTime | undefined = selfTimes.get(span.spanId);
@@ -329,11 +339,9 @@ export default class CriticalPathUtil {
}
// Sort by self-time descending (biggest contributors first)
result.sort(
(a: ServiceBreakdown, b: ServiceBreakdown) => {
return b.selfTimeUnixNano - a.selfTimeUnixNano;
},
);
result.sort((a: ServiceBreakdown, b: ServiceBreakdown) => {
return b.selfTimeUnixNano - a.selfTimeUnixNano;
});
return result;
}

View File

@@ -0,0 +1,18 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,15 @@
apiVersion: v2
name: kubernetes-agent
description: OneUptime Kubernetes Monitoring Agent — collects cluster metrics, events, and logs via OpenTelemetry and sends them to your OneUptime instance.
icon: https://raw.githubusercontent.com/OneUptime/oneuptime/master/Home/Static/img/OneUptimePNG/1.png
type: application
version: 0.1.0
appVersion: "1.0.0"
annotations:
artifacthub.io/license: MIT
artifacthub.io/category: monitoring-logging
artifacthub.io/prerelease: "false"

View File

@@ -0,0 +1,24 @@
OneUptime Kubernetes Agent has been installed.
Cluster Name: {{ .Values.clusterName }}
OneUptime URL: {{ .Values.oneuptime.url }}
The agent is now collecting:
- Node, pod, and container resource metrics (kubeletstats)
- Cluster-level metrics: deployments, replicas, pod phases (k8s_cluster)
- Kubernetes events (k8sobjects)
{{- if .Values.controlPlane.enabled }}
- Control plane metrics: etcd, API server, scheduler, controller manager (prometheus)
{{- end }}
{{- if .Values.logs.enabled }}
- Pod logs from /var/log/pods (filelog DaemonSet)
{{- end }}
To verify the agent is running:
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "kubernetes-agent.name" . }}
To check collector logs:
kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "kubernetes-agent.name" . }} -c otel-collector
Your cluster should appear in OneUptime within a few minutes at:
{{ .Values.oneuptime.url }}/dashboard/<project-id>/kubernetes

View File

@@ -0,0 +1,59 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "kubernetes-agent.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "kubernetes-agent.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "kubernetes-agent.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kubernetes-agent.labels" -}}
helm.sh/chart: {{ include "kubernetes-agent.chart" . }}
{{ include "kubernetes-agent.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: oneuptime
{{- end }}
{{/*
Selector labels
*/}}
{{- define "kubernetes-agent.selectorLabels" -}}
app.kubernetes.io/name: {{ include "kubernetes-agent.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Service account name
*/}}
{{- define "kubernetes-agent.serviceAccountName" -}}
{{- if .Values.serviceAccount.name }}
{{- .Values.serviceAccount.name }}
{{- else }}
{{- include "kubernetes-agent.fullname" . }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,140 @@
{{- if .Values.logs.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "kubernetes-agent.fullname" . }}-daemonset
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
data:
otel-collector-config.yaml: |
extensions:
health_check:
endpoint: "0.0.0.0:13133"
receivers:
# Collect pod logs from /var/log/pods
filelog:
include:
- /var/log/pods/*/*/*.log
exclude:
# Exclude the agent's own logs to avoid feedback loop
- /var/log/pods/{{ "{{ .Release.Namespace }}" }}_{{ "{{ include \"kubernetes-agent.fullname\" . }}" }}*/**/*.log
start_at: end
include_file_path: true
include_file_name: false
operators:
# Parse CRI log format
- type: router
id: get-format
routes:
- output: parser-docker
expr: 'body matches "^\\{"'
- output: parser-cri
expr: 'body matches "^[^ Z]+ "'
- output: parser-containerd
expr: 'body matches "^[^ Z]+Z"'
# Docker JSON log format
- type: json_parser
id: parser-docker
output: extract-metadata-from-filepath
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
# CRI log format
- type: regex_parser
id: parser-cri
regex: '^(?P<time>[^ Z]+) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
output: extract-metadata-from-filepath
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%L%j'
# Containerd log format
- type: regex_parser
id: parser-containerd
regex: '^(?P<time>[^ ^Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
output: extract-metadata-from-filepath
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
# Extract k8s metadata from file path
- type: regex_parser
id: extract-metadata-from-filepath
regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]+)\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log$'
parse_from: attributes["log.file.path"]
- type: move
from: attributes.log
to: body
- type: move
from: attributes.stream
to: attributes["log.iostream"]
- type: move
from: attributes.namespace
to: resource["k8s.namespace.name"]
- type: move
from: attributes.pod_name
to: resource["k8s.pod.name"]
- type: move
from: attributes.container_name
to: resource["k8s.container.name"]
- type: move
from: attributes.uid
to: resource["k8s.pod.uid"]
processors:
# Enrich with K8s metadata
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.namespace.name
- k8s.node.name
- k8s.deployment.name
- k8s.replicaset.name
- k8s.statefulset.name
- k8s.daemonset.name
- k8s.container.name
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.uid
# Stamp with cluster name
resource:
attributes:
- key: k8s.cluster.name
value: {{ .Values.clusterName | quote }}
action: upsert
batch:
send_batch_size: 1024
timeout: 10s
memory_limiter:
check_interval: 5s
limit_mib: 200
spike_limit_mib: 50
exporters:
otlphttp:
endpoint: "{{ .Values.oneuptime.url }}/otlp"
headers:
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
service:
extensions:
- health_check
pipelines:
logs:
receivers:
- filelog
processors:
- memory_limiter
- k8sattributes
- resource
- batch
exporters:
- otlphttp
{{- end }}

View File

@@ -0,0 +1,183 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "kubernetes-agent.fullname" . }}-deployment
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
data:
otel-collector-config.yaml: |
extensions:
health_check:
endpoint: "0.0.0.0:13133"
receivers:
# Collect node, pod, and container resource metrics from kubelet
kubeletstats:
collection_interval: {{ .Values.collectionInterval }}
auth_type: serviceAccount
endpoint: "https://${env:NODE_NAME}:10250"
insecure_skip_verify: true
metric_groups:
- node
- pod
- container
extra_metadata_labels:
- container.id
k8s_api_config:
auth_type: serviceAccount
# Collect cluster-level metrics from the Kubernetes API
k8s_cluster:
collection_interval: {{ .Values.collectionInterval }}
node_conditions_to_report:
- Ready
- MemoryPressure
- DiskPressure
- PIDPressure
- NetworkUnavailable
allocatable_types_to_report:
- cpu
- memory
- storage
# Watch Kubernetes events and ingest as logs
k8sobjects:
objects:
- name: events
mode: watch
group: events.k8s.io
{{- if .Values.controlPlane.enabled }}
# Scrape control plane metrics via Prometheus endpoints
prometheus:
config:
scrape_configs:
- job_name: etcd
scheme: https
tls_config:
insecure_skip_verify: {{ .Values.controlPlane.etcd.insecureSkipVerify }}
static_configs:
{{- range .Values.controlPlane.etcd.endpoints }}
- targets:
- {{ . | quote }}
{{- end }}
- job_name: kube-apiserver
scheme: https
tls_config:
insecure_skip_verify: {{ .Values.controlPlane.apiServer.insecureSkipVerify }}
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
static_configs:
{{- range .Values.controlPlane.apiServer.endpoints }}
- targets:
- {{ . | quote }}
{{- end }}
- job_name: kube-scheduler
scheme: https
tls_config:
insecure_skip_verify: {{ .Values.controlPlane.scheduler.insecureSkipVerify }}
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
static_configs:
{{- range .Values.controlPlane.scheduler.endpoints }}
- targets:
- {{ . | quote }}
{{- end }}
- job_name: kube-controller-manager
scheme: https
tls_config:
insecure_skip_verify: {{ .Values.controlPlane.controllerManager.insecureSkipVerify }}
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
static_configs:
{{- range .Values.controlPlane.controllerManager.endpoints }}
- targets:
- {{ . | quote }}
{{- end }}
{{- end }}
processors:
# Enrich all telemetry with Kubernetes metadata
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.namespace.name
- k8s.node.name
- k8s.deployment.name
- k8s.replicaset.name
- k8s.statefulset.name
- k8s.daemonset.name
- k8s.job.name
- k8s.cronjob.name
- k8s.container.name
labels:
- tag_name: k8s.pod.label.app
key: app
from: pod
- tag_name: k8s.pod.label.app.kubernetes.io/name
key: app.kubernetes.io/name
from: pod
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.ip
- sources:
- from: resource_attribute
name: k8s.pod.uid
- sources:
- from: connection
# Stamp all telemetry with the cluster name
resource:
attributes:
- key: k8s.cluster.name
value: {{ .Values.clusterName | quote }}
action: upsert
# Batch telemetry for efficient export
batch:
send_batch_size: 200
send_batch_max_size: 500
timeout: 10s
# Limit memory usage
memory_limiter:
check_interval: 5s
limit_mib: 1500
spike_limit_mib: 300
exporters:
otlphttp:
endpoint: "{{ .Values.oneuptime.url }}/otlp"
headers:
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
service:
extensions:
- health_check
pipelines:
metrics:
receivers:
- kubeletstats
- k8s_cluster
{{- if .Values.controlPlane.enabled }}
- prometheus
{{- end }}
processors:
- memory_limiter
- k8sattributes
- resource
- batch
exporters:
- otlphttp
logs:
receivers:
- k8sobjects
processors:
- memory_limiter
- k8sattributes
- resource
- batch
exporters:
- otlphttp

View File

@@ -0,0 +1,56 @@
{{- if .Values.logs.enabled }}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "kubernetes-agent.fullname" . }}-logs
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
component: log-collector
spec:
selector:
matchLabels:
{{- include "kubernetes-agent.selectorLabels" . | nindent 6 }}
component: log-collector
template:
metadata:
labels:
{{- include "kubernetes-agent.selectorLabels" . | nindent 8 }}
component: log-collector
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-daemonset.yaml") . | sha256sum }}
spec:
serviceAccountName: {{ include "kubernetes-agent.serviceAccountName" . }}
containers:
- name: otel-collector
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- "--config=/etc/otel/otel-collector-config.yaml"
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: ONEUPTIME_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "kubernetes-agent.fullname" . }}
key: api-key
resources:
{{- toYaml .Values.logs.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/otel
readOnly: true
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "kubernetes-agent.fullname" . }}-daemonset
- name: varlogpods
hostPath:
path: /var/log/pods
{{- end }}

View File

@@ -0,0 +1,67 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "kubernetes-agent.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
component: metrics-collector
spec:
replicas: {{ .Values.deployment.replicas }}
selector:
matchLabels:
{{- include "kubernetes-agent.selectorLabels" . | nindent 6 }}
component: metrics-collector
template:
metadata:
labels:
{{- include "kubernetes-agent.selectorLabels" . | nindent 8 }}
component: metrics-collector
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-deployment.yaml") . | sha256sum }}
spec:
serviceAccountName: {{ include "kubernetes-agent.serviceAccountName" . }}
containers:
- name: otel-collector
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- "--config=/etc/otel/otel-collector-config.yaml"
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CLUSTER_NAME
value: {{ .Values.clusterName | quote }}
- name: ONEUPTIME_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "kubernetes-agent.fullname" . }}
key: api-key
ports:
- name: health
containerPort: 13133
protocol: TCP
livenessProbe:
httpGet:
path: /
port: health
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: health
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.deployment.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/otel
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "kubernetes-agent.fullname" . }}-deployment

View File

@@ -0,0 +1,88 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kubernetes-agent.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kubernetes-agent.fullname" . }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
rules:
# For k8s_cluster receiver and k8sattributes processor
- apiGroups: [""]
resources:
- pods
- nodes
- nodes/proxy
- nodes/stats
- services
- endpoints
- namespaces
- events
- replicationcontrollers
- resourcequotas
- limitranges
- configmaps
- persistentvolumeclaims
- persistentvolumes
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources:
- deployments
- replicasets
- statefulsets
- daemonsets
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["get", "list", "watch"]
- apiGroups: ["autoscaling"]
resources:
- horizontalpodautoscalers
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources:
- ingresses
verbs: ["get", "list", "watch"]
- apiGroups: ["extensions"]
resources:
- ingresses
verbs: ["get", "list", "watch"]
# For k8sobjects receiver to watch events
- apiGroups: ["events.k8s.io"]
resources:
- events
verbs: ["get", "list", "watch"]
# For kubeletstats receiver
- nonResourceURLs:
- /metrics
- /metrics/cadvisor
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubernetes-agent.fullname" . }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubernetes-agent.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "kubernetes-agent.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "kubernetes-agent.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-agent.labels" . | nindent 4 }}
type: Opaque
data:
api-key: {{ .Values.oneuptime.apiKey | b64enc | quote }}

View File

@@ -0,0 +1,210 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["oneuptime", "clusterName"],
"properties": {
"oneuptime": {
"type": "object",
"description": "OneUptime instance connection details",
"required": ["url", "apiKey"],
"properties": {
"url": {
"type": "string",
"description": "URL of your OneUptime instance (e.g., https://oneuptime.example.com)"
},
"apiKey": {
"type": "string",
"description": "Project API key from OneUptime (Settings > API Keys)"
}
},
"additionalProperties": false
},
"clusterName": {
"type": "string",
"description": "Unique name for this cluster (used as k8s.cluster.name attribute)"
},
"namespaceFilters": {
"type": "object",
"description": "Namespace filters to limit which namespaces are monitored",
"properties": {
"include": {
"type": "array",
"items": { "type": "string" },
"description": "If set, only these namespaces are monitored (empty = all namespaces)"
},
"exclude": {
"type": "array",
"items": { "type": "string" },
"description": "Namespaces to exclude from monitoring"
}
},
"additionalProperties": false
},
"image": {
"type": "object",
"description": "OTel Collector image configuration",
"properties": {
"repository": {
"type": "string"
},
"tag": {
"type": "string"
},
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"]
}
},
"additionalProperties": false
},
"deployment": {
"type": "object",
"description": "Deployment (metrics + events collector) resource configuration",
"properties": {
"replicas": {
"type": "integer",
"minimum": 1
},
"resources": {
"type": "object",
"properties": {
"requests": {
"type": "object",
"properties": {
"cpu": { "type": "string" },
"memory": { "type": "string" }
},
"additionalProperties": false
},
"limits": {
"type": "object",
"properties": {
"cpu": { "type": "string" },
"memory": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"controlPlane": {
"type": "object",
"description": "Control plane monitoring configuration",
"properties": {
"enabled": {
"type": "boolean"
},
"etcd": {
"type": "object",
"properties": {
"endpoints": {
"type": "array",
"items": { "type": "string" }
},
"insecureSkipVerify": {
"type": "boolean"
}
},
"additionalProperties": false
},
"apiServer": {
"type": "object",
"properties": {
"endpoints": {
"type": "array",
"items": { "type": "string" }
},
"insecureSkipVerify": {
"type": "boolean"
}
},
"additionalProperties": false
},
"scheduler": {
"type": "object",
"properties": {
"endpoints": {
"type": "array",
"items": { "type": "string" }
},
"insecureSkipVerify": {
"type": "boolean"
}
},
"additionalProperties": false
},
"controllerManager": {
"type": "object",
"properties": {
"endpoints": {
"type": "array",
"items": { "type": "string" }
},
"insecureSkipVerify": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"logs": {
"type": "object",
"description": "Pod log collection via DaemonSet with filelog receiver",
"properties": {
"enabled": {
"type": "boolean"
},
"resources": {
"type": "object",
"properties": {
"requests": {
"type": "object",
"properties": {
"cpu": { "type": "string" },
"memory": { "type": "string" }
},
"additionalProperties": false
},
"limits": {
"type": "object",
"properties": {
"cpu": { "type": "string" },
"memory": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"collectionInterval": {
"type": "string",
"description": "Collection interval for metrics (e.g., 30s, 1m)"
},
"serviceAccount": {
"type": "object",
"description": "Service account configuration",
"properties": {
"create": {
"type": "boolean"
},
"name": {
"type": "string"
},
"annotations": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,80 @@
# OneUptime Kubernetes Agent Configuration
# Required: Your OneUptime instance connection details
oneuptime:
# URL of your OneUptime instance (e.g., https://oneuptime.example.com)
url: ""
# Project API key from OneUptime (Settings > API Keys)
apiKey: ""
# Required: Unique name for this cluster (used as k8s.cluster.name attribute)
clusterName: ""
# Namespace filters — limit which namespaces are monitored
namespaceFilters:
# If set, only these namespaces are monitored (empty = all namespaces)
include: []
# Namespaces to exclude from monitoring
exclude:
- kube-system
# OTel Collector image configuration
image:
repository: otel/opentelemetry-collector-contrib
tag: "0.96.0"
pullPolicy: IfNotPresent
# Deployment (metrics + events collector) resource configuration
deployment:
replicas: 1
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
# Control plane monitoring (etcd, API server, scheduler, controller manager)
# Disabled by default — enable for self-managed clusters.
# Managed K8s (EKS, GKE, AKS) typically don't expose control plane metrics.
controlPlane:
enabled: false
etcd:
# Endpoints to scrape etcd metrics from
endpoints:
- https://localhost:2379/metrics
# TLS configuration for etcd
insecureSkipVerify: true
apiServer:
endpoints:
- https://localhost:6443/metrics
insecureSkipVerify: true
scheduler:
endpoints:
- https://localhost:10259/metrics
insecureSkipVerify: true
controllerManager:
endpoints:
- https://localhost:10257/metrics
insecureSkipVerify: true
# Pod log collection via DaemonSet with filelog receiver
logs:
enabled: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
# Collection intervals
collectionInterval: 30s
# Service account configuration
serviceAccount:
create: true
name: ""
annotations: {}

View File

@@ -1,4 +1,4 @@
{{- if .Values.aiAgent.enabled }}
{{- if and .Values.aiAgent.enabled (not .Values.deployment.disableDeployments) }}
apiVersion: apps/v1
kind: Deployment
metadata:

View File

@@ -1,4 +1,4 @@
{{- if $.Values.app.enabled }}
{{- if and $.Values.app.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime app Deployment
apiVersion: apps/v1
kind: Deployment
@@ -137,7 +137,7 @@ spec:
---
{{- end }}
{{- if $.Values.app.enabled }}
{{- if and $.Values.app.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime app Service
{{- $appPorts := dict "port" $.Values.app.ports.http -}}
{{- $appServiceArgs := dict "ServiceName" "app" "Ports" $appPorts "Release" $.Release "Values" $.Values -}}

View File

@@ -1,4 +1,4 @@
{{- if $.Values.cronJobs.cleanup.enabled }}
{{- if and $.Values.cronJobs.cleanup.enabled (not $.Values.deployment.disableDeployments) }}
apiVersion: batch/v1
kind: CronJob

View File

@@ -1,4 +1,4 @@
{{- if $.Values.home.enabled }}
{{- if and $.Values.home.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime Home Deployment
apiVersion: apps/v1
kind: Deployment
@@ -126,7 +126,7 @@ spec:
{{- end }}
{{- if $.Values.home.enabled }}
{{- if and $.Values.home.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime home Service
{{- $homePorts := $.Values.home.ports -}}
{{- $homeServiceArgs := dict "ServiceName" "home" "Ports" $homePorts "Release" $.Release "Values" $.Values -}}

View File

@@ -3,7 +3,7 @@ KEDA ScaledObjects for various services
*/}}
{{/* Telemetry KEDA ScaledObject */}}
{{- if and .Values.keda.enabled .Values.telemetry.enabled .Values.telemetry.keda.enabled (not .Values.telemetry.disableAutoscaler) }}
{{- if and .Values.keda.enabled .Values.telemetry.enabled .Values.telemetry.keda.enabled (not .Values.telemetry.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
{{- $metricsConfig := dict "enabled" .Values.telemetry.keda.enabled "minReplicas" .Values.telemetry.keda.minReplicas "maxReplicas" .Values.telemetry.keda.maxReplicas "pollingInterval" .Values.telemetry.keda.pollingInterval "cooldownPeriod" .Values.telemetry.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_telemetry_queue_size" "threshold" .Values.telemetry.keda.queueSizeThreshold "port" .Values.telemetry.ports.http)) }}
{{- $telemetryKedaArgs := dict "ServiceName" "telemetry" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.telemetry.disableAutoscaler }}
{{- include "oneuptime.kedaScaledObject" $telemetryKedaArgs }}
@@ -12,7 +12,7 @@ KEDA ScaledObjects for various services
{{/* Probe KEDA ScaledObjects - one for each probe configuration */}}
{{- range $key, $val := $.Values.probes }}
{{- $probeEnabled := or (not (hasKey $val "enabled")) $val.enabled }}
{{- if and $.Values.keda.enabled $probeEnabled (and $val.keda $val.keda.enabled) (not $val.disableAutoscaler) }}
{{- if and $.Values.keda.enabled $probeEnabled (and $val.keda $val.keda.enabled) (not $val.disableAutoscaler) (not $.Values.deployment.disableDeployments) }}
{{- $serviceName := printf "probe-%s" $key }}
{{- $probePort := 3874 }}
{{- if and $val.ports $val.ports.http }}
@@ -25,14 +25,14 @@ KEDA ScaledObjects for various services
{{- end }}
{{/* Worker KEDA ScaledObject */}}
{{- if and .Values.keda.enabled .Values.worker.enabled .Values.worker.keda.enabled (not .Values.worker.disableAutoscaler) }}
{{- if and .Values.keda.enabled .Values.worker.enabled .Values.worker.keda.enabled (not .Values.worker.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
{{- $metricsConfig := dict "enabled" .Values.worker.keda.enabled "minReplicas" .Values.worker.keda.minReplicas "maxReplicas" .Values.worker.keda.maxReplicas "pollingInterval" .Values.worker.keda.pollingInterval "cooldownPeriod" .Values.worker.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_worker_queue_size" "threshold" .Values.worker.keda.queueSizeThreshold "port" .Values.worker.ports.http)) }}
{{- $workerKedaArgs := dict "ServiceName" "worker" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.worker.disableAutoscaler }}
{{- include "oneuptime.kedaScaledObject" $workerKedaArgs }}
{{- end }}
{{/* AI Agent KEDA ScaledObject */}}
{{- if and .Values.keda.enabled .Values.aiAgent.enabled .Values.aiAgent.keda.enabled (not .Values.aiAgent.disableAutoscaler) }}
{{- if and .Values.keda.enabled .Values.aiAgent.enabled .Values.aiAgent.keda.enabled (not .Values.aiAgent.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
{{- $metricsConfig := dict "enabled" .Values.aiAgent.keda.enabled "minReplicas" .Values.aiAgent.keda.minReplicas "maxReplicas" .Values.aiAgent.keda.maxReplicas "pollingInterval" .Values.aiAgent.keda.pollingInterval "cooldownPeriod" .Values.aiAgent.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_ai_agent_queue_size" "threshold" .Values.aiAgent.keda.queueSizeThreshold "port" .Values.aiAgent.ports.http)) }}
{{- $aiAgentKedaArgs := dict "ServiceName" "ai-agent" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.aiAgent.disableAutoscaler }}
{{- include "oneuptime.kedaScaledObject" $aiAgentKedaArgs }}

View File

@@ -1,4 +1,4 @@
{{- if $.Values.nginx.enabled }}
{{- if and $.Values.nginx.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime nginx Deployment
apiVersion: apps/v1

View File

@@ -1,3 +1,4 @@
{{- if not $.Values.deployment.disableDeployments }}
{{- range $key, $val := $.Values.probes }}
{{- if or (not (hasKey $val "enabled")) $val.enabled }}
apiVersion: apps/v1
@@ -166,3 +167,4 @@ spec:
---
{{- end }}
{{- end }}

View File

@@ -1,4 +1,4 @@
{{- if $.Values.telemetry.enabled }}
{{- if and $.Values.telemetry.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime telemetry Deployment
apiVersion: apps/v1
@@ -137,7 +137,7 @@ spec:
---
{{- if $.Values.telemetry.enabled }}
{{- if and $.Values.telemetry.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime telemetry Service
{{- $telemetryPorts := dict "http" $.Values.telemetry.ports.http "grpc" $.Values.telemetry.ports.grpc -}}
{{- $telemetryServiceArgs := dict "ServiceName" "telemetry" "Ports" $telemetryPorts "Release" $.Release "Values" $.Values -}}

View File

@@ -1,4 +1,4 @@
{{- if $.Values.testServer.enabled }}
{{- if and $.Values.testServer.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime test-server Deployment
{{- $testServerPorts := $.Values.testServer.ports -}}

View File

@@ -1,4 +1,4 @@
{{- if $.Values.worker.enabled }}
{{- if and $.Values.worker.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime worker Deployment
apiVersion: apps/v1
kind: Deployment
@@ -128,7 +128,7 @@ spec:
---
{{- if $.Values.worker.enabled }}
{{- if and $.Values.worker.enabled (not $.Values.deployment.disableDeployments) }}
# OneUptime worker Service
{{- $workerPorts := $.Values.worker.ports -}}
{{- $workerServiceArgs := dict "ServiceName" "worker" "Ports" $workerPorts "Release" $.Release "Values" $.Values -}}

View File

@@ -86,6 +86,11 @@
"deployment": {
"type": "object",
"properties": {
"disableDeployments": {
"type": "boolean",
"description": "When set to true, no OneUptime deployments are provisioned. Only databases (Redis, ClickHouse, Postgres) will be running.",
"default": false
},
"includeTimestampLabel": {
"type": "boolean"
},

View File

@@ -48,6 +48,8 @@ externalSecrets:
passwordKey:
deployment:
# When set to true, no OneUptime deployments are provisioned. Only databases (Redis, ClickHouse, Postgres) will be running.
disableDeployments: false
# Default replica count for all deployments
replicaCount: 1
# Update strategy type for all deployments

View File

@@ -4,7 +4,13 @@ import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "../theme";
import GradientButton from "./GradientButton";
type EmptyIcon = "incidents" | "alerts" | "episodes" | "notes" | "monitors" | "default";
type EmptyIcon =
| "incidents"
| "alerts"
| "episodes"
| "notes"
| "monitors"
| "default";
interface EmptyStateProps {
title: string;

View File

@@ -17,11 +17,14 @@ function toDisplayString(val: unknown): string {
}
if (typeof val === "object") {
// Handle OneUptime typed objects like URL { _type, value }
const obj = val as Record<string, unknown>;
const obj: Record<string, unknown> = val as Record<string, unknown>;
if (typeof obj.value === "string") {
return obj.value;
}
if (typeof obj.toString === "function" && obj.toString !== Object.prototype.toString) {
if (
typeof obj.toString === "function" &&
obj.toString !== Object.prototype.toString
) {
return obj.toString();
}
try {
@@ -671,7 +674,7 @@ export default function MonitorSummaryView({
return getProbeResponse(probe);
}, [probeItems, selectedProbeIndex]);
const renderContent = (): React.JSX.Element => {
const renderContent: () => React.JSX.Element = (): React.JSX.Element => {
if (!latestResponse) {
return (
<View style={{ padding: 20, alignItems: "center" }}>

View File

@@ -142,13 +142,12 @@ export function useAllProjectCounts(): UseAllProjectCountsResult {
0,
);
const inoperationalMonitorCount: number =
inoperationalMonitorQueries.reduce(
(sum: number, q: UseQueryResult<ListResponse<MonitorItem>, Error>) => {
return sum + (q.data?.count ?? 0);
},
0,
);
const inoperationalMonitorCount: number = inoperationalMonitorQueries.reduce(
(sum: number, q: UseQueryResult<ListResponse<MonitorItem>, Error>) => {
return sum + (q.data?.count ?? 0);
},
0,
);
const isLoading: boolean =
incidentQuery.isPending ||

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