feat: Add Kubernetes Cluster Management and Monitoring Agent

- Implemented a new migration for the KubernetesCluster and KubernetesClusterLabel tables in the database.
- Created a KubernetesClusterService for managing cluster instances, including methods for finding or creating clusters, updating their status, and marking disconnected clusters.
- Introduced a Helm chart for the OneUptime Kubernetes Monitoring Agent, including configuration files, deployment templates, and RBAC settings.
- Added support for collecting metrics and logs from Kubernetes clusters using OpenTelemetry.
- Configured service accounts, secrets, and resource limits for the agent's deployment and daemonset.
- Provided detailed notes and helper templates for the Helm chart to facilitate installation and configuration.
This commit is contained in:
Nawaz Dhandala
2026-03-17 15:29:52 +00:00
parent da6c749d96
commit bc9949abe4
39 changed files with 4106 additions and 0 deletions

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

@@ -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.Cube,
iconColor: "teal",
category: "Observability",
},
// Automation & Analytics
{
title: "Dashboards",

View File

@@ -0,0 +1,125 @@
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 } 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";
const KubernetesClusters: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
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.",
}}
noItemsMessage="No Kubernetes clusters connected yet."
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,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,31 @@
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,
},
],
},
];
return <SideMenu sections={sections} />;
};
export default KubernetesSideMenu;

View File

@@ -0,0 +1,301 @@
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 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);
// etcd metrics (scraped via prometheus receiver)
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
// API Server request rate
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Sum,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
// API Server request latency
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
// Scheduler pending pods
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
// Scheduler latency
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
// Controller Manager work queue depth
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: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
};
const [etcdMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [etcdDbSizeQuery],
formulaConfigs: [],
});
const [apiServerMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [apiServerRequestRateQuery, apiServerLatencyQuery],
formulaConfigs: [],
});
const [schedulerMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [schedulerPendingQuery, schedulerLatencyQuery],
formulaConfigs: [],
});
const [controllerMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [controllerQueueDepthQuery],
formulaConfigs: [],
});
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,253 @@
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";
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: {
startValue: startDate,
endValue: endDate,
} as any,
},
limit: 200,
skip: 0,
select: {
time: true,
body: true,
severityText: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
});
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["k8s.cluster.name"] !== item.clusterIdentifier &&
attrs["k8s_cluster_name"] !== item.clusterIdentifier
) {
continue;
}
// k8sobjects receiver events have k8s event attributes
const eventType: string =
(attrs["k8s.event.type"] as string) ||
(attrs["type"] as string) ||
"";
const reason: string =
(attrs["k8s.event.reason"] as string) ||
(attrs["reason"] as string) ||
"";
const objectKind: string =
(attrs["k8s.object.kind"] as string) ||
(attrs["involvedObject.kind"] as string) ||
"";
const objectName: string =
(attrs["k8s.object.name"] as string) ||
(attrs["involvedObject.name"] as string) ||
"";
const namespace: string =
(attrs["k8s.namespace.name"] as string) ||
(attrs["namespace"] as string) ||
"";
if (eventType || reason || objectKind) {
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: log.body || "",
});
}
}
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,329 @@
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 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";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
const KubernetesClusterOverview: 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,
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 clusterIdentifier: string = cluster.clusterIdentifier || "";
// Time range: past 6 hours
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 => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const nodeName: string =
(attributes["k8s.node.name"] as string) || "Unknown Node";
return { title: nodeName };
};
// CPU utilization metric query
const cpuQueryConfig: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_cpu",
title: "Node CPU Utilization",
description: "CPU utilization across cluster nodes",
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
// Memory utilization metric query
const memoryQueryConfig: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "node_memory",
title: "Node Memory Usage",
description: "Memory usage across cluster nodes",
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.node.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
// Pod CPU usage
const podCpuQueryConfig: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_cpu",
title: "Pod CPU Usage (Top Consumers)",
description: "CPU usage by pod across the cluster",
legend: "CPU",
legendUnit: "%",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: (data: AggregateModel): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const podName: string =
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
const namespace: string =
(attributes["k8s.namespace.name"] as string) || "";
return { title: namespace ? `${namespace}/${podName}` : podName };
},
};
// Pod Memory usage
const podMemoryQueryConfig: MetricQueryConfigData = {
metricAliasData: {
metricVariable: "pod_memory",
title: "Pod Memory Usage (Top Consumers)",
description: "Memory usage by pod across the cluster",
legend: "Memory",
legendUnit: "bytes",
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: (data: AggregateModel): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const podName: string =
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
const namespace: string =
(attributes["k8s.namespace.name"] as string) || "";
return { title: namespace ? `${namespace}/${podName}` : podName };
},
};
const [metricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [
cpuQueryConfig,
memoryQueryConfig,
podCpuQueryConfig,
podMemoryQueryConfig,
],
formulaConfigs: [],
});
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 Overview"
cardProps={{
title: "Cluster Details",
description: "Basic information about this Kubernetes cluster.",
}}
isEditable={false}
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: {
provider: true,
},
title: "Provider",
fieldType: FieldType.Text,
},
{
field: {
lastSeenAt: true,
},
title: "Last Seen",
fieldType: FieldType.DateTime,
},
],
}}
/>
{/* Resource Utilization Charts */}
<Card
title="Resource Utilization"
description="CPU and memory usage trends across the cluster over the last 6 hours."
>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={() => {}}
/>
</Card>
</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,213 @@
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 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";
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
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 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 getNodeSeries = (data: AggregateModel): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const nodeName: string =
(attributes["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: {
"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: {
"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: {
"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.receive",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getNodeSeries,
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [
nodeCpuQuery,
nodeMemoryQuery,
nodeFilesystemQuery,
nodeNetworkRxQuery,
],
formulaConfigs: [],
});
return (
<Fragment>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
nodeCpuQuery,
nodeMemoryQuery,
nodeFilesystemQuery,
nodeNetworkRxQuery,
],
formulaConfigs: [],
});
}}
/>
</Fragment>
);
};
export default KubernetesClusterNodes;

View File

@@ -0,0 +1,218 @@
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";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
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 => {
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,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 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";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
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 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 getPodSeries = (data: AggregateModel): ChartSeries => {
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const podName: string =
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
const namespace: string =
(attributes["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: {
"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: {
"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.receive",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
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.transmit",
attributes: {
"k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
},
groupBy: {
attributes: true,
},
},
getSeries: getPodSeries,
};
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
podNetworkRxQuery,
podNetworkTxQuery,
],
formulaConfigs: [],
});
return (
<Fragment>
<MetricView
data={metricViewData}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
podNetworkRxQuery,
podNetworkTxQuery,
],
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,103 @@
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: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Settings}
/>
<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>
<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.Settings}
/>
</SideMenuSection>
</SideMenu>
);
};
export default KubernetesClusterSideMenu;

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

@@ -0,0 +1,137 @@
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 KubernetesClusterViewSettings from "../Pages/Kubernetes/View/Settings";
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>
<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_SETTINGS)}
element={
<KubernetesClusterViewSettings
{...props}
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS] as Route}
/>
}
/>
</PageRoute>
</Routes>
);
};
export default KubernetesRoutes;

View File

@@ -0,0 +1,64 @@
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_SETTINGS, [
"Project",
"Kubernetes",
"View Cluster",
"Settings",
]),
};
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

@@ -214,6 +214,19 @@ 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",
// Code Repository
CODE_REPOSITORY_ROOT = "CODE_REPOSITORY_ROOT",
CODE_REPOSITORY = "CODE_REPOSITORY",

View File

@@ -59,6 +59,18 @@ 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`,
};
export const WorkflowRoutePath: Dictionary<string> = {
[PageMap.WORKFLOWS_LOGS]: "logs",
[PageMap.WORKFLOWS_VARIABLES]: "variables",
@@ -1465,6 +1477,70 @@ 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]
}`,
),
// Dashboards
[PageMap.DASHBOARDS_ROOT]: new Route(

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,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,7 @@ 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";
export default [
InitialMigration,
@@ -536,4 +537,5 @@ export default [
MigrationName1773344537755,
MigrationName1773402621107,
MigrationName1773676206197,
MigrationName1774000000000,
];

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

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

@@ -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,134 @@
{{- 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: |
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 }}"
headers:
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
service:
pipelines:
logs:
receivers:
- filelog
processors:
- memory_limiter
- k8sattributes
- resource
- batch
exporters:
- otlphttp
{{- end }}

View File

@@ -0,0 +1,176 @@
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: |
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: 1024
timeout: 10s
# Limit memory usage
memory_limiter:
check_interval: 5s
limit_mib: 400
spike_limit_mib: 100
exporters:
otlphttp:
endpoint: "{{ .Values.oneuptime.url }}"
headers:
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
service:
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,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: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# 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: {}