mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Merge pull request #2360 from OneUptime/k8s-impl
feat: Add Kubernetes Cluster Management and Monitoring Agent
This commit is contained in:
@@ -6,6 +6,13 @@
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "cd MobileApp && npx expo start --port 8081"],
|
||||
"port": 8081
|
||||
},
|
||||
{
|
||||
"name": "dashboard",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "cd App/FeatureSet/Dashboard && npm run dev"],
|
||||
"port": 3002,
|
||||
"autoPort": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3
.github/workflows/common-jobs.yaml
vendored
3
.github/workflows/common-jobs.yaml
vendored
@@ -18,9 +18,10 @@ jobs:
|
||||
- name: Install Helm
|
||||
run: |
|
||||
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
- name: Lint Helm Chart
|
||||
- name: Lint Helm Chart
|
||||
run: |
|
||||
helm lint ./HelmChart/Public/oneuptime
|
||||
helm lint ./HelmChart/Public/kubernetes-agent
|
||||
|
||||
js-lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -134,11 +134,16 @@ jobs:
|
||||
helm lint oneuptime
|
||||
helm template oneuptime --values oneuptime/values.yaml
|
||||
helm package --sign --key 'key@oneuptime.com' --keyring ~/.gnupg/secring.gpg oneuptime --version ${{needs.read-version.outputs.major_minor}} --app-version ${{needs.read-version.outputs.major_minor}}
|
||||
echo "Helm Chart Package created successfully"
|
||||
echo "OneUptime Helm Chart Package created successfully"
|
||||
helm lint kubernetes-agent
|
||||
helm template kubernetes-agent --values kubernetes-agent/values.yaml
|
||||
helm package --sign --key 'key@oneuptime.com' --keyring ~/.gnupg/secring.gpg kubernetes-agent --version ${{needs.read-version.outputs.major_minor}} --app-version ${{needs.read-version.outputs.major_minor}}
|
||||
echo "Kubernetes Agent Helm Chart Package created successfully"
|
||||
cd ..
|
||||
ls
|
||||
echo "Copying the package to helm-chart repo"
|
||||
rm -r ../../helm-chart/oneuptime
|
||||
rm -rf ../../helm-chart/oneuptime
|
||||
rm -rf ../../helm-chart/kubernetes-agent
|
||||
cp -r ./Public/* ../../helm-chart
|
||||
echo "Package copied successfully"
|
||||
cd .. && cd .. && cd helm-chart
|
||||
|
||||
@@ -233,6 +233,9 @@ import IncidentTemplateOwnerUserService, {
|
||||
import IncidentTemplateService, {
|
||||
Service as IncidentTemplateServiceType,
|
||||
} from "Common/Server/Services/IncidentTemplateService";
|
||||
import KubernetesClusterService, {
|
||||
Service as KubernetesClusterServiceType,
|
||||
} from "Common/Server/Services/KubernetesClusterService";
|
||||
import LabelService, {
|
||||
Service as LabelServiceType,
|
||||
} from "Common/Server/Services/LabelService";
|
||||
@@ -552,6 +555,7 @@ import IncidentTemplate from "Common/Models/DatabaseModels/IncidentTemplate";
|
||||
import IncidentTemplateOwnerTeam from "Common/Models/DatabaseModels/IncidentTemplateOwnerTeam";
|
||||
import IncidentTemplateOwnerUser from "Common/Models/DatabaseModels/IncidentTemplateOwnerUser";
|
||||
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import MonitorCustomField from "Common/Models/DatabaseModels/MonitorCustomField";
|
||||
import MonitorGroupOwnerTeam from "Common/Models/DatabaseModels/MonitorGroupOwnerTeam";
|
||||
@@ -1859,6 +1863,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new BaseAPI<Label, LabelServiceType>(Label, LabelService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<KubernetesCluster, KubernetesClusterServiceType>(
|
||||
KubernetesCluster,
|
||||
KubernetesClusterService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<EmailVerificationToken, EmailVerificationTokenServiceType>(
|
||||
|
||||
@@ -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() || ""}
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface ComponentProps {
|
||||
query: Query<TelemetryException>;
|
||||
title: string;
|
||||
description: string;
|
||||
onFetchSuccess?:
|
||||
| ((data: Array<TelemetryException>, totalCount: number) => void)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const TelemetryExceptionTable: FunctionComponent<ComponentProps> = (
|
||||
@@ -47,6 +50,7 @@ const TelemetryExceptionTable: FunctionComponent<ComponentProps> = (
|
||||
userPreferencesKey="telemetry-exception-table"
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
onFetchSuccess={props.onFetchSuccess}
|
||||
singularName="Exception"
|
||||
pluralName="Exceptions"
|
||||
name="TelemetryException"
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import TelemetryIngestionKey from "Common/Models/DatabaseModels/TelemetryIngestionKey";
|
||||
import ModelAPI, { ListResult } from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import { HOST, HTTP_PROTOCOL } from "Common/UI/Config";
|
||||
import ModelFormModal from "Common/UI/Components/ModelFormModal/ModelFormModal";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import { FormType } from "Common/UI/Components/Forms/ModelForm";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import Dropdown, {
|
||||
DropdownOption,
|
||||
DropdownValue,
|
||||
} from "Common/UI/Components/Dropdown/Dropdown";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import { getKubernetesInstallationMarkdown } from "../../Pages/Kubernetes/Utils/DocumentationMarkdown";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
|
||||
export interface ComponentProps {
|
||||
clusterName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const KubernetesDocumentationCard: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
// Ingestion key state
|
||||
const [ingestionKeys, setIngestionKeys] = useState<
|
||||
Array<TelemetryIngestionKey>
|
||||
>([]);
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string>("");
|
||||
const [isLoadingKeys, setIsLoadingKeys] = useState<boolean>(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState<boolean>(false);
|
||||
const [keyError, setKeyError] = useState<string>("");
|
||||
|
||||
// Compute OneUptime URL
|
||||
const httpProtocol: string =
|
||||
HTTP_PROTOCOL === Protocol.HTTPS ? "https" : "http";
|
||||
const oneuptimeUrl: string = HOST
|
||||
? `${httpProtocol}://${HOST}`
|
||||
: "<YOUR_ONEUPTIME_URL>";
|
||||
|
||||
// Fetch ingestion keys on mount
|
||||
useEffect(() => {
|
||||
loadIngestionKeys().catch(() => {});
|
||||
}, []);
|
||||
|
||||
const loadIngestionKeys: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoadingKeys(true);
|
||||
setKeyError("");
|
||||
const result: ListResult<TelemetryIngestionKey> =
|
||||
await ModelAPI.getList<TelemetryIngestionKey>({
|
||||
modelType: TelemetryIngestionKey,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
limit: 50,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
secretKey: true,
|
||||
description: true,
|
||||
},
|
||||
sort: {},
|
||||
});
|
||||
|
||||
setIngestionKeys(result.data);
|
||||
|
||||
// Auto-select the first key if available and none selected
|
||||
if (result.data.length > 0 && !selectedKeyId) {
|
||||
setSelectedKeyId(result.data[0]!.id?.toString() || "");
|
||||
}
|
||||
} catch (err) {
|
||||
setKeyError(API.getFriendlyErrorMessage(err as Error));
|
||||
} finally {
|
||||
setIsLoadingKeys(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get the selected key object
|
||||
const selectedKey: TelemetryIngestionKey | undefined = useMemo(() => {
|
||||
return ingestionKeys.find((k: TelemetryIngestionKey) => {
|
||||
return k.id?.toString() === selectedKeyId;
|
||||
});
|
||||
}, [ingestionKeys, selectedKeyId]);
|
||||
|
||||
// Get API key for code snippets
|
||||
const apiKeyValue: string =
|
||||
selectedKey?.secretKey?.toString() || "<YOUR_API_KEY>";
|
||||
|
||||
const renderKeySelector: () => ReactElement = (): ReactElement => {
|
||||
if (isLoadingKeys) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (keyError) {
|
||||
return <ErrorMessage message={keyError} />;
|
||||
}
|
||||
|
||||
if (ingestionKeys.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-sm font-medium text-gray-900 mb-1">
|
||||
No ingestion keys yet
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Create an ingestion key to authenticate your Kubernetes agent.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors shadow-sm"
|
||||
>
|
||||
<Icon icon={IconProp.Add} className="w-4 h-4" />
|
||||
Create Ingestion Key
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Key selector row */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex-1">
|
||||
<Dropdown
|
||||
options={ingestionKeys.map(
|
||||
(key: TelemetryIngestionKey): DropdownOption => {
|
||||
return {
|
||||
value: key.id?.toString() || "",
|
||||
label: key.name || "Unnamed Key",
|
||||
};
|
||||
},
|
||||
)}
|
||||
value={
|
||||
ingestionKeys
|
||||
.filter((key: TelemetryIngestionKey) => {
|
||||
return key.id?.toString() === selectedKeyId;
|
||||
})
|
||||
.map((key: TelemetryIngestionKey): DropdownOption => {
|
||||
return {
|
||||
value: key.id?.toString() || "",
|
||||
label: key.name || "Unnamed Key",
|
||||
};
|
||||
})[0]
|
||||
}
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
if (value) {
|
||||
setSelectedKeyId(value.toString());
|
||||
}
|
||||
}}
|
||||
placeholder="Select an ingestion key"
|
||||
ariaLabel="Select ingestion key"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors flex-shrink-0"
|
||||
>
|
||||
<Icon icon={IconProp.Add} className="w-4 h-4" />
|
||||
New Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Credentials display */}
|
||||
{selectedKey && (
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="grid grid-cols-1 divide-y divide-gray-100">
|
||||
<div className="px-4 py-3 flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-md bg-blue-50 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Icon
|
||||
icon={IconProp.Globe}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
OneUptime URL
|
||||
</div>
|
||||
<div className="text-sm text-gray-900 font-mono mt-0.5 break-all select-all">
|
||||
{oneuptimeUrl}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-3 flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-md bg-amber-50 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Icon
|
||||
icon={IconProp.Key}
|
||||
className="w-4 h-4 text-amber-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
API Key
|
||||
</div>
|
||||
<div className="text-sm text-gray-900 font-mono mt-0.5 break-all select-all">
|
||||
{selectedKey.secretKey?.toString() || "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const installationMarkdown: string = getKubernetesInstallationMarkdown({
|
||||
clusterName: props.clusterName,
|
||||
oneuptimeUrl: oneuptimeUrl,
|
||||
apiKey: apiKeyValue,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card title={props.title} description={props.description}>
|
||||
<div className="px-4 pb-6">
|
||||
{/* Ingestion Key Section */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-2">
|
||||
Select Ingestion Key
|
||||
</label>
|
||||
{renderKeySelector()}
|
||||
</div>
|
||||
|
||||
{/* Documentation */}
|
||||
<MarkdownViewer text={installationMarkdown} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Create Ingestion Key Modal */}
|
||||
{showCreateModal && (
|
||||
<ModelFormModal<TelemetryIngestionKey>
|
||||
modelType={TelemetryIngestionKey}
|
||||
name="Create Ingestion Key"
|
||||
title="Create Ingestion Key"
|
||||
description="Create a new telemetry ingestion key for authenticating your Kubernetes agent."
|
||||
onClose={() => {
|
||||
setShowCreateModal(false);
|
||||
}}
|
||||
submitButtonText="Create Key"
|
||||
onSuccess={(item: TelemetryIngestionKey) => {
|
||||
setShowCreateModal(false);
|
||||
loadIngestionKeys()
|
||||
.then(() => {
|
||||
if (item.id) {
|
||||
setSelectedKeyId(item.id.toString());
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}}
|
||||
formProps={{
|
||||
name: "Create Ingestion Key",
|
||||
modelType: TelemetryIngestionKey,
|
||||
id: "create-ingestion-key",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "e.g. Kubernetes Agent Key",
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Optional description for this key",
|
||||
},
|
||||
],
|
||||
formType: FormType.Create,
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: TelemetryIngestionKey,
|
||||
): Promise<TelemetryIngestionKey> => {
|
||||
item.projectId = ProjectUtil.getCurrentProjectId()!;
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesDocumentationCard;
|
||||
@@ -70,6 +70,8 @@ export interface ComponentProps {
|
||||
noLogsMessage?: string | undefined;
|
||||
logQuery?: Query<Log> | undefined;
|
||||
limit?: number | undefined;
|
||||
onCountChange?: ((count: number) => void) | undefined;
|
||||
onShowDocumentation?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE: number = 100;
|
||||
@@ -427,6 +429,10 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
|
||||
setLogs(listResult.data);
|
||||
setTotalCount(listResult.count);
|
||||
|
||||
if (props.onCountChange) {
|
||||
props.onCountChange(listResult.count);
|
||||
}
|
||||
|
||||
const maximumPage: number = Math.max(
|
||||
1,
|
||||
Math.ceil(listResult.count / Math.max(pageSize, 1)),
|
||||
@@ -1236,6 +1242,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
|
||||
onFieldValueSelect={handleFieldValueSelect}
|
||||
timeRange={timeRange}
|
||||
onTimeRangeChange={handleTimeRangeChange}
|
||||
onShowDocumentation={props.onShowDocumentation}
|
||||
selectedColumns={selectedColumns}
|
||||
onSelectedColumnsChange={(columns: Array<string>) => {
|
||||
setSelectedColumns(normalizeLogsTableColumns(columns));
|
||||
|
||||
@@ -17,6 +17,9 @@ import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"
|
||||
|
||||
export interface ComponentProps {
|
||||
serviceIds?: Array<ObjectID> | undefined;
|
||||
onFetchSuccess?:
|
||||
| ((data: Array<MetricType>, totalCount: number) => void)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const MetricsTable: FunctionComponent<ComponentProps> = (
|
||||
@@ -113,6 +116,7 @@ const MetricsTable: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
showViewIdButton={false}
|
||||
noItemsMessage={"No metrics found for this service."}
|
||||
onFetchSuccess={props.onFetchSuccess}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
filters={[
|
||||
|
||||
@@ -144,6 +144,17 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
iconColor: "indigo",
|
||||
category: "Observability",
|
||||
},
|
||||
// {
|
||||
// title: "Kubernetes",
|
||||
// description: "Monitor Kubernetes clusters.",
|
||||
// route: RouteUtil.populateRouteParams(
|
||||
// RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
|
||||
// ),
|
||||
// activeRoute: RouteMap[PageMap.KUBERNETES_CLUSTERS],
|
||||
// icon: IconProp.Kubernetes,
|
||||
// iconColor: "blue",
|
||||
// category: "Observability",
|
||||
// },
|
||||
// Automation & Analytics
|
||||
{
|
||||
title: "Dashboards",
|
||||
|
||||
@@ -622,8 +622,7 @@ const SpanViewer: FunctionComponent<ComponentProps> = (
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
{link.attributes &&
|
||||
Object.keys(link.attributes).length > 0 ? (
|
||||
{link.attributes && Object.keys(link.attributes).length > 0 ? (
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 font-medium mb-1">
|
||||
Attributes
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,8 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
|
||||
const [hoveredSpanId, setHoveredSpanId] = React.useState<string | null>(null);
|
||||
const [focusedSpanId, setFocusedSpanId] = React.useState<string | null>(null);
|
||||
const containerRef: React.RefObject<HTMLDivElement | null> = React.useRef<HTMLDivElement>(null);
|
||||
const containerRef: React.RefObject<HTMLDivElement | null> =
|
||||
React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Build span data for critical path utility
|
||||
const spanDataList: SpanData[] = React.useMemo(() => {
|
||||
@@ -90,7 +91,7 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
}
|
||||
}
|
||||
|
||||
const getServiceInfo = (
|
||||
const getServiceInfo: (span: Span) => { color: Color; name: string } = (
|
||||
span: Span,
|
||||
): { color: Color; name: string } => {
|
||||
const service: Service | undefined = telemetryServices.find(
|
||||
@@ -104,7 +105,10 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
};
|
||||
};
|
||||
|
||||
const buildNode = (span: Span, depth: number): FlameGraphNode => {
|
||||
const buildNode: (span: Span, depth: number) => FlameGraphNode = (
|
||||
span: Span,
|
||||
depth: number,
|
||||
): FlameGraphNode => {
|
||||
const children: Span[] = childrenMap.get(span.spanId!) || [];
|
||||
const selfTime: SpanSelfTime | undefined = selfTimes.get(span.spanId!);
|
||||
const serviceInfo: { color: Color; name: string } = getServiceInfo(span);
|
||||
@@ -123,7 +127,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
startTimeUnixNano: span.startTimeUnixNano!,
|
||||
endTimeUnixNano: span.endTimeUnixNano!,
|
||||
durationUnixNano: span.durationUnixNano!,
|
||||
selfTimeUnixNano: selfTime ? selfTime.selfTimeUnixNano : span.durationUnixNano!,
|
||||
selfTimeUnixNano: selfTime
|
||||
? selfTime.selfTimeUnixNano
|
||||
: span.durationUnixNano!,
|
||||
serviceColor: serviceInfo.color,
|
||||
serviceName: serviceInfo.name,
|
||||
};
|
||||
@@ -155,7 +161,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
// Find max depth for height calculation
|
||||
const maxDepth: number = React.useMemo(() => {
|
||||
let max: number = 0;
|
||||
const traverse = (node: FlameGraphNode): void => {
|
||||
const traverse: (node: FlameGraphNode) => void = (
|
||||
node: FlameGraphNode,
|
||||
): void => {
|
||||
if (node.depth > max) {
|
||||
max = node.depth;
|
||||
}
|
||||
@@ -175,7 +183,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
return { viewStart: traceStart, viewEnd: traceEnd };
|
||||
}
|
||||
|
||||
const findNode = (nodes: FlameGraphNode[]): FlameGraphNode | null => {
|
||||
const findNode: (nodes: FlameGraphNode[]) => FlameGraphNode | null = (
|
||||
nodes: FlameGraphNode[],
|
||||
): FlameGraphNode | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.span.spanId === focusedSpanId) {
|
||||
return node;
|
||||
@@ -210,7 +220,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
const renderNode = (node: FlameGraphNode): ReactElement | null => {
|
||||
const renderNode: (node: FlameGraphNode) => ReactElement | null = (
|
||||
node: FlameGraphNode,
|
||||
): ReactElement | null => {
|
||||
// Calculate position relative to view
|
||||
const nodeStart: number = Math.max(node.startTimeUnixNano, viewStart);
|
||||
const nodeEnd: number = Math.min(node.endTimeUnixNano, viewEnd);
|
||||
@@ -220,13 +232,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
}
|
||||
|
||||
const leftPercent: number =
|
||||
totalDuration > 0
|
||||
? ((nodeStart - viewStart) / totalDuration) * 100
|
||||
: 0;
|
||||
totalDuration > 0 ? ((nodeStart - viewStart) / totalDuration) * 100 : 0;
|
||||
const widthPercent: number =
|
||||
totalDuration > 0
|
||||
? ((nodeEnd - nodeStart) / totalDuration) * 100
|
||||
: 0;
|
||||
totalDuration > 0 ? ((nodeEnd - nodeStart) / totalDuration) * 100 : 0;
|
||||
|
||||
const isHovered: boolean = hoveredSpanId === node.span.spanId;
|
||||
const isSelected: boolean = selectedSpanId === node.span.spanId;
|
||||
@@ -302,7 +310,9 @@ const FlameGraph: FunctionComponent<FlameGraphProps> = (
|
||||
if (!hoveredSpanId) {
|
||||
return null;
|
||||
}
|
||||
const findNode = (nodes: FlameGraphNode[]): FlameGraphNode | null => {
|
||||
const findNode: (nodes: FlameGraphNode[]) => FlameGraphNode | null = (
|
||||
nodes: FlameGraphNode[],
|
||||
): FlameGraphNode | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.span.spanId === hoveredSpanId) {
|
||||
return node;
|
||||
|
||||
@@ -712,7 +712,13 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}, [spans, showErrorsOnly, selectedServiceIds, spanSearchText, telemetryServices]);
|
||||
}, [
|
||||
spans,
|
||||
showErrorsOnly,
|
||||
selectedServiceIds,
|
||||
spanSearchText,
|
||||
telemetryServices,
|
||||
]);
|
||||
|
||||
// Search match count for display
|
||||
const searchMatchCount: number = React.useMemo(() => {
|
||||
@@ -1288,11 +1294,9 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowCriticalPath(
|
||||
(prev: boolean) => {
|
||||
return !prev;
|
||||
},
|
||||
);
|
||||
setShowCriticalPath((prev: boolean) => {
|
||||
return !prev;
|
||||
});
|
||||
}}
|
||||
className={`text-xs font-medium px-3 py-1.5 rounded-md border transition-all flex items-center space-x-1 ${
|
||||
showCriticalPath
|
||||
@@ -1428,17 +1432,14 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{serviceBreakdown.map((breakdown: ServiceBreakdown) => {
|
||||
const service: Service | undefined =
|
||||
telemetryServices.find((s: Service) => {
|
||||
return (
|
||||
s._id?.toString() === breakdown.serviceId
|
||||
);
|
||||
});
|
||||
const serviceName: string =
|
||||
service?.name || "Unknown";
|
||||
const service: Service | undefined = telemetryServices.find(
|
||||
(s: Service) => {
|
||||
return s._id?.toString() === breakdown.serviceId;
|
||||
},
|
||||
);
|
||||
const serviceName: string = service?.name || "Unknown";
|
||||
const serviceColor: string = String(
|
||||
(service?.serviceColor as unknown as string) ||
|
||||
"#6366f1",
|
||||
(service?.serviceColor as unknown as string) || "#6366f1",
|
||||
);
|
||||
const percent: number = Math.min(
|
||||
breakdown.percentOfTrace,
|
||||
@@ -1446,7 +1447,10 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={breakdown.serviceId} className="flex items-center space-x-2">
|
||||
<div
|
||||
key={breakdown.serviceId}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<span
|
||||
className="h-2.5 w-2.5 rounded-sm ring-1 ring-black/10 flex-shrink-0"
|
||||
style={{
|
||||
@@ -1468,8 +1472,7 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500 w-20 text-right">
|
||||
{SpanUtil.getSpanDurationAsString({
|
||||
spanDurationInUnixNano:
|
||||
breakdown.selfTimeUnixNano,
|
||||
spanDurationInUnixNano: breakdown.selfTimeUnixNano,
|
||||
divisibilityFactor: divisibilityFactor,
|
||||
})}{" "}
|
||||
({percent.toFixed(1)}%)
|
||||
@@ -1526,9 +1529,7 @@ const TraceExplorer: FunctionComponent<ComponentProps> = (
|
||||
setSelectedSpans([spanId]);
|
||||
}}
|
||||
selectedSpanId={
|
||||
selectedSpans.length > 0
|
||||
? selectedSpans[0]
|
||||
: undefined
|
||||
selectedSpans.length > 0 ? selectedSpans[0] : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,8 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
|
||||
return maxEnd - minStart;
|
||||
}, [spans]);
|
||||
|
||||
const divisibilityFactor = SpanUtil.getDivisibilityFactor(traceDuration);
|
||||
const divisibilityFactor: number =
|
||||
SpanUtil.getDivisibilityFactor(traceDuration);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
@@ -135,8 +136,10 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
// Layout: arrange nodes in a topological order based on edges
|
||||
// Simple layout: find entry nodes and lay out left-to-right
|
||||
/*
|
||||
* Layout: arrange nodes in a topological order based on edges
|
||||
* Simple layout: find entry nodes and lay out left-to-right
|
||||
*/
|
||||
const { nodePositions, layoutWidth, layoutHeight } = React.useMemo(() => {
|
||||
// Build adjacency list
|
||||
const adjList: Map<string, string[]> = new Map();
|
||||
@@ -151,10 +154,7 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
|
||||
const neighbors: string[] = adjList.get(edge.fromServiceId) || [];
|
||||
neighbors.push(edge.toServiceId);
|
||||
adjList.set(edge.fromServiceId, neighbors);
|
||||
inDegree.set(
|
||||
edge.toServiceId,
|
||||
(inDegree.get(edge.toServiceId) || 0) + 1,
|
||||
);
|
||||
inDegree.set(edge.toServiceId, (inDegree.get(edge.toServiceId) || 0) + 1);
|
||||
}
|
||||
|
||||
// Topological sort using BFS (Kahn's algorithm)
|
||||
@@ -333,18 +333,16 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
|
||||
refY="3"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0 0, 8 3, 0 6"
|
||||
fill="#9ca3af"
|
||||
/>
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#9ca3af" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Render nodes */}
|
||||
{nodes.map((node: ServiceNode) => {
|
||||
const pos: { x: number; y: number } | undefined =
|
||||
nodePositions.get(node.serviceId);
|
||||
const pos: { x: number; y: number } | undefined = nodePositions.get(
|
||||
node.serviceId,
|
||||
);
|
||||
if (!pos) {
|
||||
return null;
|
||||
}
|
||||
@@ -355,9 +353,7 @@ const TraceServiceMap: FunctionComponent<TraceServiceMapProps> = (
|
||||
<div
|
||||
key={node.serviceId}
|
||||
className={`absolute rounded-lg border-2 bg-white shadow-sm p-3 ${
|
||||
hasErrors
|
||||
? "border-red-300"
|
||||
: "border-gray-200"
|
||||
hasErrors ? "border-red-300" : "border-gray-200"
|
||||
}`}
|
||||
style={{
|
||||
left: `${pos.x}px`,
|
||||
|
||||
@@ -44,6 +44,9 @@ export interface ComponentProps {
|
||||
spanQuery?: Query<Span> | undefined;
|
||||
isMinimalTable?: boolean | undefined;
|
||||
noItemsMessage?: string | undefined;
|
||||
onFetchSuccess?:
|
||||
| ((data: Array<Span>, totalCount: number) => void)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const TraceTable: FunctionComponent<ComponentProps> = (
|
||||
@@ -298,6 +301,7 @@ const TraceTable: FunctionComponent<ComponentProps> = (
|
||||
noItemsMessage={
|
||||
props.noItemsMessage ? props.noItemsMessage : "No spans found."
|
||||
}
|
||||
onFetchSuccess={props.onFetchSuccess}
|
||||
showRefreshButton={true}
|
||||
sortBy="startTime"
|
||||
sortOrder={SortOrder.Descending}
|
||||
@@ -310,12 +314,9 @@ const TraceTable: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}
|
||||
return Promise.resolve(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACE_VIEW]!,
|
||||
{
|
||||
modelId: span.traceId!.toString(),
|
||||
},
|
||||
),
|
||||
RouteUtil.populateRouteParams(RouteMap[PageMap.TRACE_VIEW]!, {
|
||||
modelId: span.traceId!.toString(),
|
||||
}),
|
||||
);
|
||||
}}
|
||||
filters={[
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
|
||||
const ExceptionsDocumentationPage: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps,
|
||||
): ReactElement => {
|
||||
return <TelemetryDocumentation telemetryType="exceptions" />;
|
||||
};
|
||||
|
||||
export default ExceptionsDocumentationPage;
|
||||
@@ -51,6 +51,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Book,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionsTable from "../../Components/Exceptions/ExceptionsTable";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -9,12 +20,48 @@ const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
|
||||
const disableTelemetryForThisProject: boolean =
|
||||
props.currentProject?.reseller?.enableTelemetryFeatures === false;
|
||||
|
||||
const [serviceCount, setServiceCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: Service,
|
||||
query: {},
|
||||
});
|
||||
setServiceCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (disableTelemetryForThisProject) {
|
||||
return (
|
||||
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (serviceCount === 0) {
|
||||
return <TelemetryDocumentation telemetryType="exceptions" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExceptionsTable
|
||||
query={{
|
||||
|
||||
180
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx
Normal file
180
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import KubernetesDocumentationCard from "../../Components/Kubernetes/DocumentationCard";
|
||||
|
||||
const KubernetesClusters: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const [clusterCount, setClusterCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchClusterCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: KubernetesCluster,
|
||||
query: {},
|
||||
});
|
||||
setClusterCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchClusterCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (clusterCount === 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
<KubernetesDocumentationCard
|
||||
clusterName="my-cluster"
|
||||
title="Getting Started with Kubernetes Monitoring"
|
||||
description="No Kubernetes clusters connected yet. Install the agent using the guide below and your cluster will appear here automatically."
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelTable<KubernetesCluster>
|
||||
modelType={KubernetesCluster}
|
||||
id="kubernetes-clusters-table"
|
||||
userPreferencesKey="kubernetes-clusters-table"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
name="Kubernetes Clusters"
|
||||
isViewable={true}
|
||||
filters={[]}
|
||||
cardProps={{
|
||||
title: "Kubernetes Clusters",
|
||||
description:
|
||||
"Clusters being monitored in this project. Install the OneUptime kubernetes-agent Helm chart to connect a cluster.",
|
||||
}}
|
||||
showViewIdButton={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east-1",
|
||||
description:
|
||||
"This should match the clusterName value in your kubernetes-agent Helm chart.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Production cluster running in US East",
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
otelCollectorStatus: true,
|
||||
},
|
||||
title: "Status",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
nodeCount: true,
|
||||
},
|
||||
title: "Nodes",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
podCount: true,
|
||||
},
|
||||
title: "Pods",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
]}
|
||||
onViewPage={(item: KubernetesCluster): Promise<Route> => {
|
||||
return Promise.resolve(
|
||||
new Route(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
|
||||
{
|
||||
modelId: item._id,
|
||||
},
|
||||
).toString(),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusters;
|
||||
@@ -0,0 +1,19 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import KubernetesDocumentationCard from "../../Components/Kubernetes/DocumentationCard";
|
||||
|
||||
const KubernetesDocumentation: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<KubernetesDocumentationCard
|
||||
clusterName="my-cluster"
|
||||
title="Agent Installation Guide"
|
||||
description="Install the OneUptime Kubernetes Agent using Helm to connect your cluster. Once installed, the cluster will appear automatically."
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesDocumentation;
|
||||
25
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Layout.tsx
Normal file
25
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Layout.tsx
Normal 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;
|
||||
40
App/FeatureSet/Dashboard/src/Pages/Kubernetes/SideMenu.tsx
Normal file
40
App/FeatureSet/Dashboard/src/Pages/Kubernetes/SideMenu.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import SideMenu, {
|
||||
SideMenuSectionProps,
|
||||
} from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const KubernetesSideMenu: FunctionComponent = (): ReactElement => {
|
||||
const sections: SideMenuSectionProps[] = [
|
||||
{
|
||||
title: "Kubernetes",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "All Clusters",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.List,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_DOCUMENTATION] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Book,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
};
|
||||
|
||||
export default KubernetesSideMenu;
|
||||
@@ -0,0 +1,144 @@
|
||||
export interface KubernetesInstallationMarkdownOptions {
|
||||
clusterName: string;
|
||||
oneuptimeUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export function getKubernetesInstallationMarkdown(
|
||||
options: KubernetesInstallationMarkdownOptions,
|
||||
): string {
|
||||
const { clusterName, oneuptimeUrl, apiKey } = options;
|
||||
|
||||
return `
|
||||
## Prerequisites
|
||||
|
||||
- A running Kubernetes cluster (v1.23+)
|
||||
- \`kubectl\` configured to access your cluster
|
||||
- \`helm\` v3 installed
|
||||
|
||||
## Step 1: Add the OneUptime Helm Repository
|
||||
|
||||
\`\`\`bash
|
||||
helm repo add oneuptime https://helm.oneuptime.com
|
||||
helm repo update
|
||||
\`\`\`
|
||||
|
||||
## Step 2: Install the Kubernetes Agent
|
||||
|
||||
\`\`\`bash
|
||||
helm install kubernetes-agent oneuptime/kubernetes-agent \\
|
||||
--namespace oneuptime-agent \\
|
||||
--create-namespace \\
|
||||
--set oneuptime.url="${oneuptimeUrl}" \\
|
||||
--set oneuptime.apiKey="${apiKey}" \\
|
||||
--set clusterName="${clusterName}"
|
||||
\`\`\`
|
||||
|
||||
## Step 3: Verify the Installation
|
||||
|
||||
Check that the agent pods are running:
|
||||
|
||||
\`\`\`bash
|
||||
kubectl get pods -n oneuptime-agent
|
||||
\`\`\`
|
||||
|
||||
You should see a **Deployment** pod (for metrics and events collection) and **DaemonSet** pods (one per node, for log collection):
|
||||
|
||||
\`\`\`
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
kubernetes-agent-deployment-xxxxx-xxxxx 1/1 Running 0 1m
|
||||
kubernetes-agent-daemonset-xxxxx 1/1 Running 0 1m
|
||||
\`\`\`
|
||||
|
||||
Once the agent connects, your cluster will appear automatically in the Kubernetes section.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Namespace Filtering
|
||||
|
||||
By default, \`kube-system\` is excluded. To monitor only specific namespaces:
|
||||
|
||||
\`\`\`bash
|
||||
helm install kubernetes-agent oneuptime/kubernetes-agent \\
|
||||
--namespace oneuptime-agent \\
|
||||
--create-namespace \\
|
||||
--set oneuptime.url="${oneuptimeUrl}" \\
|
||||
--set oneuptime.apiKey="${apiKey}" \\
|
||||
--set clusterName="${clusterName}" \\
|
||||
--set "namespaceFilters.include={default,production,staging}"
|
||||
\`\`\`
|
||||
|
||||
### Disable Log Collection
|
||||
|
||||
If you only need metrics and events (no pod logs):
|
||||
|
||||
\`\`\`bash
|
||||
helm install kubernetes-agent oneuptime/kubernetes-agent \\
|
||||
--namespace oneuptime-agent \\
|
||||
--create-namespace \\
|
||||
--set oneuptime.url="${oneuptimeUrl}" \\
|
||||
--set oneuptime.apiKey="${apiKey}" \\
|
||||
--set clusterName="${clusterName}" \\
|
||||
--set logs.enabled=false
|
||||
\`\`\`
|
||||
|
||||
### Enable Control Plane Monitoring
|
||||
|
||||
For self-managed clusters (not EKS/GKE/AKS), you can enable control plane metrics:
|
||||
|
||||
\`\`\`bash
|
||||
helm install kubernetes-agent oneuptime/kubernetes-agent \\
|
||||
--namespace oneuptime-agent \\
|
||||
--create-namespace \\
|
||||
--set oneuptime.url="${oneuptimeUrl}" \\
|
||||
--set oneuptime.apiKey="${apiKey}" \\
|
||||
--set clusterName="${clusterName}" \\
|
||||
--set controlPlane.enabled=true
|
||||
\`\`\`
|
||||
|
||||
> **Note:** Managed Kubernetes services (EKS, GKE, AKS) typically do not expose control plane metrics. Only enable this for self-managed clusters.
|
||||
|
||||
## Upgrading the Agent
|
||||
|
||||
\`\`\`bash
|
||||
helm repo update
|
||||
helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
|
||||
--namespace oneuptime-agent
|
||||
\`\`\`
|
||||
|
||||
## Uninstalling the Agent
|
||||
|
||||
\`\`\`bash
|
||||
helm uninstall kubernetes-agent --namespace oneuptime-agent
|
||||
kubectl delete namespace oneuptime-agent
|
||||
\`\`\`
|
||||
|
||||
## What Gets Collected
|
||||
|
||||
The OneUptime Kubernetes Agent collects:
|
||||
|
||||
| Category | Data |
|
||||
|----------|------|
|
||||
| **Node Metrics** | CPU utilization, memory usage, filesystem usage, network I/O |
|
||||
| **Pod Metrics** | CPU usage, memory usage, network I/O, restarts |
|
||||
| **Container Metrics** | CPU usage, memory usage per container |
|
||||
| **Cluster Metrics** | Node conditions, allocatable resources, pod counts |
|
||||
| **Kubernetes Events** | Warnings, errors, scheduling events |
|
||||
| **Pod Logs** | stdout/stderr logs from all containers (via DaemonSet) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent shows "Disconnected"
|
||||
|
||||
1. Check that the agent pods are running: \`kubectl get pods -n oneuptime-agent\`
|
||||
2. Check the agent logs: \`kubectl logs -n oneuptime-agent deployment/kubernetes-agent-deployment\`
|
||||
3. Verify your OneUptime URL and API key are correct
|
||||
4. Ensure your cluster can reach the OneUptime instance over the network
|
||||
|
||||
### No metrics appearing
|
||||
|
||||
1. Check that the cluster identifier matches: this cluster uses **\`${clusterName}\`**
|
||||
2. Verify the RBAC permissions: \`kubectl get clusterrolebinding | grep kubernetes-agent\`
|
||||
3. Check the OTel collector logs for export errors
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const KubernetesClusterControlPlane: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [etcdMetricViewData, setEtcdMetricViewData] =
|
||||
useState<MetricViewData | null>(null);
|
||||
const [apiServerMetricViewData, setApiServerMetricViewData] =
|
||||
useState<MetricViewData | null>(null);
|
||||
const [schedulerMetricViewData, setSchedulerMetricViewData] =
|
||||
useState<MetricViewData | null>(null);
|
||||
const [controllerMetricViewData, setControllerMetricViewData] =
|
||||
useState<MetricViewData | null>(null);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const etcdDbSizeQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "etcd_db_size",
|
||||
title: "etcd Database Size",
|
||||
description: "Total size of the etcd database",
|
||||
legend: "DB Size",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "etcd_mvcc_db_total_size_in_bytes",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const apiServerRequestRateQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "apiserver_requests",
|
||||
title: "API Server Request Rate",
|
||||
description: "Total API server requests by verb",
|
||||
legend: "Requests",
|
||||
legendUnit: "req/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "apiserver_request_total",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Sum,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const apiServerLatencyQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "apiserver_latency",
|
||||
title: "API Server Request Latency",
|
||||
description: "API server request duration",
|
||||
legend: "Latency",
|
||||
legendUnit: "seconds",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "apiserver_request_duration_seconds",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const schedulerPendingQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "scheduler_pending",
|
||||
title: "Scheduler Pending Pods",
|
||||
description: "Number of pods pending scheduling",
|
||||
legend: "Pending Pods",
|
||||
legendUnit: "",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "scheduler_pending_pods",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const schedulerLatencyQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "scheduler_latency",
|
||||
title: "Scheduler Latency",
|
||||
description: "End-to-end scheduling latency",
|
||||
legend: "Latency",
|
||||
legendUnit: "seconds",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "scheduler_e2e_scheduling_duration_seconds",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const controllerQueueDepthQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "controller_queue",
|
||||
title: "Controller Manager Queue Depth",
|
||||
description: "Work queue depth for controller manager",
|
||||
legend: "Queue Depth",
|
||||
legendUnit: "",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "workqueue_depth",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setEtcdMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [etcdDbSizeQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
setApiServerMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [apiServerRequestRateQuery, apiServerLatencyQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
setSchedulerMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [schedulerPendingQuery, schedulerLatencyQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
setControllerMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [controllerQueueDepthQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}, [cluster]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (
|
||||
!cluster ||
|
||||
!etcdMetricViewData ||
|
||||
!apiServerMetricViewData ||
|
||||
!schedulerMetricViewData ||
|
||||
!controllerMetricViewData
|
||||
) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-700">
|
||||
Control plane metrics require the <code>controlPlane.enabled</code>{" "}
|
||||
flag to be set to <code>true</code> in the kubernetes-agent Helm chart
|
||||
values. This is typically only available for self-managed Kubernetes
|
||||
clusters, not managed services like EKS, GKE, or AKS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="etcd"
|
||||
description="etcd is the consistent, distributed key-value store used as the backing store for all cluster data."
|
||||
>
|
||||
<MetricView
|
||||
data={etcdMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="API Server"
|
||||
description="The Kubernetes API server validates and configures data for API objects and serves REST operations."
|
||||
>
|
||||
<MetricView
|
||||
data={apiServerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Scheduler"
|
||||
description="The scheduler watches for newly created pods that have no node assigned and selects a node for them to run on."
|
||||
>
|
||||
<MetricView
|
||||
data={schedulerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Controller Manager"
|
||||
description="The controller manager runs controller processes that regulate the state of the cluster."
|
||||
>
|
||||
<MetricView
|
||||
data={controllerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterControlPlane;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,78 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import KubernetesDocumentationCard from "../../../Components/Kubernetes/DocumentationCard";
|
||||
|
||||
const KubernetesClusterDocumentation: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
name: true,
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterName: string =
|
||||
cluster.clusterIdentifier || cluster.name || "my-cluster";
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<KubernetesDocumentationCard
|
||||
clusterName={clusterName}
|
||||
title="Agent Installation Guide"
|
||||
description="Follow these steps to install the OneUptime Kubernetes Agent on your cluster."
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterDocumentation;
|
||||
339
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx
Normal file
339
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
|
||||
interface KubernetesEvent {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
reason: string;
|
||||
objectKind: string;
|
||||
objectName: string;
|
||||
namespace: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const KubernetesClusterEvents: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [events, setEvents] = useState<Array<KubernetesEvent>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchData: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
|
||||
if (!item?.clusterIdentifier) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
|
||||
|
||||
const listResult: ListResult<Log> = await AnalyticsModelAPI.getList<Log>({
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
|
||||
time: new InBetween<Date>(startDate, endDate),
|
||||
},
|
||||
limit: 200,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
body: true,
|
||||
severityText: true,
|
||||
attributes: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
});
|
||||
|
||||
// Helper to extract a string value from OTLP kvlistValue
|
||||
const getKvValue: (
|
||||
kvList: JSONObject | undefined,
|
||||
key: string,
|
||||
) => string = (kvList: JSONObject | undefined, key: string): string => {
|
||||
if (!kvList) {
|
||||
return "";
|
||||
}
|
||||
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
|
||||
"values"
|
||||
] as Array<JSONObject> | undefined;
|
||||
if (!values) {
|
||||
return "";
|
||||
}
|
||||
for (const entry of values) {
|
||||
if (entry["key"] === key) {
|
||||
const val: JSONObject | undefined = entry["value"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
if (val["stringValue"]) {
|
||||
return val["stringValue"] as string;
|
||||
}
|
||||
if (val["intValue"]) {
|
||||
return String(val["intValue"]);
|
||||
}
|
||||
// Nested kvlist (e.g., regarding, metadata)
|
||||
if (val["kvlistValue"]) {
|
||||
return val["kvlistValue"] as unknown as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper to get nested kvlist value
|
||||
const getNestedKvValue: (
|
||||
kvList: JSONObject | undefined,
|
||||
parentKey: string,
|
||||
childKey: string,
|
||||
) => string = (
|
||||
kvList: JSONObject | undefined,
|
||||
parentKey: string,
|
||||
childKey: string,
|
||||
): string => {
|
||||
if (!kvList) {
|
||||
return "";
|
||||
}
|
||||
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
|
||||
"values"
|
||||
] as Array<JSONObject> | undefined;
|
||||
if (!values) {
|
||||
return "";
|
||||
}
|
||||
for (const entry of values) {
|
||||
if (entry["key"] === parentKey) {
|
||||
const val: JSONObject | undefined = entry["value"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (val && val["kvlistValue"]) {
|
||||
return getKvValue(val["kvlistValue"] as JSONObject, childKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const k8sEvents: Array<KubernetesEvent> = [];
|
||||
|
||||
for (const log of listResult.data) {
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
|
||||
// Filter to only k8s events from this cluster
|
||||
if (
|
||||
attrs["resource.k8s.cluster.name"] !== item.clusterIdentifier &&
|
||||
attrs["k8s.cluster.name"] !== item.clusterIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process k8s event logs (from k8sobjects receiver)
|
||||
if (attrs["logAttributes.event.domain"] !== "k8s") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the body which is OTLP kvlistValue JSON
|
||||
let bodyObj: JSONObject | null = null;
|
||||
try {
|
||||
if (typeof log.body === "string") {
|
||||
bodyObj = JSON.parse(log.body) as JSONObject;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!bodyObj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys
|
||||
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (!topKvList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the "object" which is the actual k8s Event
|
||||
const objectKvListRaw: string = getKvValue(topKvList, "object");
|
||||
if (!objectKvListRaw || typeof objectKvListRaw === "string") {
|
||||
continue;
|
||||
}
|
||||
const objectKvList: JSONObject =
|
||||
objectKvListRaw as unknown as JSONObject;
|
||||
|
||||
const eventType: string = getKvValue(objectKvList, "type") || "";
|
||||
const reason: string = getKvValue(objectKvList, "reason") || "";
|
||||
const note: string = getKvValue(objectKvList, "note") || "";
|
||||
|
||||
// Get object details from "regarding" sub-object
|
||||
const objectKind: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "kind") || "";
|
||||
const objectName: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "name") || "";
|
||||
const namespace: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "namespace") ||
|
||||
getNestedKvValue(objectKvList, "metadata", "namespace") ||
|
||||
"";
|
||||
|
||||
if (eventType || reason) {
|
||||
k8sEvents.push({
|
||||
timestamp: log.time
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
|
||||
: "",
|
||||
type: eventType || "Unknown",
|
||||
reason: reason || "Unknown",
|
||||
objectKind: objectKind || "Unknown",
|
||||
objectName: objectName || "Unknown",
|
||||
namespace: namespace || "default",
|
||||
message: note || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setEvents(k8sEvents);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card
|
||||
title="Kubernetes Events"
|
||||
description="Events from the last 24 hours collected by the k8sobjects receiver. Warning events may indicate issues that need attention."
|
||||
>
|
||||
{events.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">
|
||||
No Kubernetes events found in the last 24 hours. Events will appear
|
||||
here once the kubernetes-agent is sending data.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Time
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Reason
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Object
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Namespace
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Message
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{events.map((event: KubernetesEvent, index: number) => {
|
||||
const isWarning: boolean =
|
||||
event.type.toLowerCase() === "warning";
|
||||
return (
|
||||
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.timestamp}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
isWarning
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-green-100 text-green-800"
|
||||
}`}
|
||||
>
|
||||
{event.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{event.reason}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{event.objectKind}/{event.objectName}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.namespace}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500 max-w-md truncate">
|
||||
{event.message}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterEvents;
|
||||
215
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx
Normal file
215
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const KubernetesClusterOverview: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
name: true,
|
||||
clusterIdentifier: true,
|
||||
provider: true,
|
||||
otelCollectorStatus: true,
|
||||
lastSeenAt: true,
|
||||
nodeCount: true,
|
||||
podCount: true,
|
||||
namespaceCount: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const statusColor: string =
|
||||
cluster.otelCollectorStatus === "connected"
|
||||
? "text-green-600"
|
||||
: "text-red-600";
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
|
||||
<InfoCard
|
||||
title="Nodes"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.nodeCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Pods"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.podCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Namespaces"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.namespaceCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Agent Status"
|
||||
value={
|
||||
<span className={`text-2xl font-semibold ${statusColor}`}>
|
||||
{cluster.otelCollectorStatus === "connected"
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cluster Details */}
|
||||
<CardModelDetail<KubernetesCluster>
|
||||
name="Cluster Details"
|
||||
cardProps={{
|
||||
title: "Cluster Details",
|
||||
description: "Basic information about this Kubernetes cluster.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Cluster"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Production cluster running in US East",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east-1",
|
||||
description:
|
||||
"This should match the clusterName value in your kubernetes-agent Helm chart.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: false,
|
||||
placeholder: "EKS, GKE, AKS, etc.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: KubernetesCluster,
|
||||
id: "kubernetes-cluster-overview",
|
||||
modelId: modelId,
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Cluster Name",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
lastSeenAt: true,
|
||||
},
|
||||
title: "Last Seen",
|
||||
fieldType: FieldType.DateTime,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterOverview;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
220
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx
Normal file
220
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
|
||||
const KubernetesClusterNodes: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getNodeSeries: (data: AggregateModel) => ChartSeries = (
|
||||
data: AggregateModel,
|
||||
): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const nodeName: string =
|
||||
(attributes["resource.k8s.node.name"] as string) || "Unknown Node";
|
||||
return { title: nodeName };
|
||||
};
|
||||
|
||||
const nodeCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_cpu",
|
||||
title: "Node CPU Utilization",
|
||||
description: "CPU utilization by node",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.cpu.utilization",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_memory",
|
||||
title: "Node Memory Usage",
|
||||
description: "Memory usage by node",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.memory.usage",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeFilesystemQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_filesystem",
|
||||
title: "Node Filesystem Usage",
|
||||
description: "Filesystem usage by node",
|
||||
legend: "Filesystem",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.filesystem.usage",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeNetworkRxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_network_rx",
|
||||
title: "Node Network Receive",
|
||||
description: "Network bytes received by node",
|
||||
legend: "Network RX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.network.io",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
"metricAttributes.direction": "receive",
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
setMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
nodeCpuQuery,
|
||||
nodeMemoryQuery,
|
||||
nodeFilesystemQuery,
|
||||
nodeNetworkRxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}, [cluster]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster || !metricViewData) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: metricViewData.queryConfigs,
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterNodes;
|
||||
226
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx
Normal file
226
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
|
||||
const KubernetesClusterPodDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
|
||||
const podName: string = Navigation.getLastParam()?.toString() || "";
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getContainerSeries: (data: AggregateModel) => ChartSeries = (
|
||||
data: AggregateModel,
|
||||
): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const containerName: string =
|
||||
(attributes["k8s.container.name"] as string) || "Unknown Container";
|
||||
return { title: containerName };
|
||||
};
|
||||
|
||||
const cpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "container_cpu",
|
||||
title: "Container CPU Utilization",
|
||||
description: `CPU utilization for containers in pod ${podName}`,
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "container.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getContainerSeries,
|
||||
};
|
||||
|
||||
const memoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "container_memory",
|
||||
title: "Container Memory Usage",
|
||||
description: `Memory usage for containers in pod ${podName}`,
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "container.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getContainerSeries,
|
||||
};
|
||||
|
||||
const podCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_cpu",
|
||||
title: "Pod CPU Utilization",
|
||||
description: `CPU utilization for pod ${podName}`,
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const podMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_memory",
|
||||
title: "Pod Memory Usage",
|
||||
description: `Memory usage for pod ${podName}`,
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Pod Name" value={podName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Pod Metrics: ${podName}`}
|
||||
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterPodDetail;
|
||||
223
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx
Normal file
223
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
|
||||
const KubernetesClusterPods: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getPodSeries: (data: AggregateModel) => ChartSeries = (
|
||||
data: AggregateModel,
|
||||
): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const podName: string =
|
||||
(attributes["resource.k8s.pod.name"] as string) || "Unknown Pod";
|
||||
const namespace: string =
|
||||
(attributes["resource.k8s.namespace.name"] as string) || "";
|
||||
return { title: namespace ? `${namespace}/${podName}` : podName };
|
||||
};
|
||||
|
||||
const podCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_cpu",
|
||||
title: "Pod CPU Utilization",
|
||||
description: "CPU utilization by pod",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.cpu.utilization",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_memory",
|
||||
title: "Pod Memory Usage",
|
||||
description: "Memory usage by pod",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.memory.usage",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podNetworkRxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_network_rx",
|
||||
title: "Pod Network Receive",
|
||||
description: "Network bytes received by pod",
|
||||
legend: "Network RX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.network.io",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
"metricAttributes.direction": "receive",
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podNetworkTxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_network_tx",
|
||||
title: "Pod Network Transmit",
|
||||
description: "Network bytes transmitted by pod",
|
||||
legend: "Network TX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.network.io",
|
||||
attributes: {
|
||||
"resource.k8s.cluster.name": clusterIdentifier,
|
||||
"metricAttributes.direction": "transmit",
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
setMetricViewData({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
podNetworkRxQuery,
|
||||
podNetworkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}, [cluster]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster || !metricViewData) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: metricViewData.queryConfigs,
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterPods;
|
||||
@@ -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;
|
||||
106
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx
Normal file
106
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import SideMenu from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem";
|
||||
import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
}
|
||||
|
||||
const KubernetesClusterSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Documentation",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Book}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Resources">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Pods",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Circle}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Nodes",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Server}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Observability">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Events",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Logs}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Control Plane",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Activity}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Delete Cluster",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Trash}
|
||||
className="danger-on-hover"
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterSideMenu;
|
||||
@@ -1,7 +1,18 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import DashboardLogsViewer from "../../Components/Logs/LogsViewer";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const LogsPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -9,12 +20,60 @@ const LogsPage: FunctionComponent<PageComponentProps> = (
|
||||
const disableTelemetryForThisProject: boolean =
|
||||
props.currentProject?.reseller?.enableTelemetryFeatures === false;
|
||||
|
||||
const [serviceCount, setServiceCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [showDocs, setShowDocs] = useState<boolean>(false);
|
||||
|
||||
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: Service,
|
||||
query: {},
|
||||
});
|
||||
setServiceCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (disableTelemetryForThisProject) {
|
||||
return (
|
||||
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showDocs) {
|
||||
return (
|
||||
<TelemetryDocumentation
|
||||
telemetryType="logs"
|
||||
onClose={() => {
|
||||
setShowDocs(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (serviceCount === 0) {
|
||||
return <TelemetryDocumentation telemetryType="logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardLogsViewer
|
||||
showFilters={true}
|
||||
@@ -22,6 +81,9 @@ const LogsPage: FunctionComponent<PageComponentProps> = (
|
||||
limit={100}
|
||||
enableRealtime={true}
|
||||
id="logs"
|
||||
onShowDocumentation={() => {
|
||||
setShowDocs(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
11
App/FeatureSet/Dashboard/src/Pages/Metrics/Documentation.tsx
Normal file
11
App/FeatureSet/Dashboard/src/Pages/Metrics/Documentation.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
|
||||
const MetricsDocumentationPage: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps,
|
||||
): ReactElement => {
|
||||
return <TelemetryDocumentation telemetryType="metrics" />;
|
||||
};
|
||||
|
||||
export default MetricsDocumentationPage;
|
||||
@@ -1,7 +1,18 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import MetricsTable from "../../Components/Metrics/MetricsTable";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const MetricsPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -9,12 +20,48 @@ const MetricsPage: FunctionComponent<PageComponentProps> = (
|
||||
const disableTelemetryForThisProject: boolean =
|
||||
props.currentProject?.reseller?.enableTelemetryFeatures === false;
|
||||
|
||||
const [serviceCount, setServiceCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: Service,
|
||||
query: {},
|
||||
});
|
||||
setServiceCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (disableTelemetryForThisProject) {
|
||||
return (
|
||||
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (serviceCount === 0) {
|
||||
return <TelemetryDocumentation telemetryType="metrics" />;
|
||||
}
|
||||
|
||||
return <MetricsTable />;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.METRICS_DOCUMENTATION] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Book,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
|
||||
56
App/FeatureSet/Dashboard/src/Pages/Settings/MobileApps.tsx
Normal file
56
App/FeatureSet/Dashboard/src/Pages/Settings/MobileApps.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const MobileApps: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<Card
|
||||
title="OneUptime On-Call - iOS"
|
||||
description="Download the OneUptime On-Call app for iOS to receive push notifications and manage on-call schedules from your iPhone or iPad."
|
||||
buttons={[
|
||||
{
|
||||
title: "Download on the App Store",
|
||||
icon: IconProp.Download,
|
||||
onClick: () => {
|
||||
window.open(
|
||||
"https://apps.apple.com/us/app/oneuptime-on-call/id6759615391",
|
||||
"_blank",
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="OneUptime On-Call - Android"
|
||||
description="Download the OneUptime On-Call app for Android to receive push notifications and manage on-call schedules from your Android device."
|
||||
buttons={[
|
||||
{
|
||||
title: "Download on Google Play",
|
||||
icon: IconProp.Download,
|
||||
onClick: () => {
|
||||
window.open(
|
||||
"https://play.google.com/store/apps/details?id=com.oneuptime.oncall",
|
||||
"_blank",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Download APK",
|
||||
icon: IconProp.Download,
|
||||
onClick: () => {
|
||||
window.open(
|
||||
"https://github.com/OneUptime/oneuptime/releases/latest/download/oneuptime-on-call-android-app.apk",
|
||||
"_blank",
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileApps;
|
||||
@@ -147,6 +147,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
},
|
||||
icon: IconProp.Bell,
|
||||
},
|
||||
{
|
||||
link: {
|
||||
title: "Mobile Apps",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_MOBILE_APPS] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.DevicePhoneMobile,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
11
App/FeatureSet/Dashboard/src/Pages/Traces/Documentation.tsx
Normal file
11
App/FeatureSet/Dashboard/src/Pages/Traces/Documentation.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
|
||||
const TracesDocumentationPage: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps,
|
||||
): ReactElement => {
|
||||
return <TelemetryDocumentation telemetryType="traces" />;
|
||||
};
|
||||
|
||||
export default TracesDocumentationPage;
|
||||
@@ -1,7 +1,18 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import TraceTable from "../../Components/Traces/TraceTable";
|
||||
import Service from "Common/Models/DatabaseModels/Service";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const TracesPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -9,12 +20,48 @@ const TracesPage: FunctionComponent<PageComponentProps> = (
|
||||
const disableTelemetryForThisProject: boolean =
|
||||
props.currentProject?.reseller?.enableTelemetryFeatures === false;
|
||||
|
||||
const [serviceCount, setServiceCount] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const count: number = await ModelAPI.count({
|
||||
modelType: Service,
|
||||
query: {},
|
||||
});
|
||||
setServiceCount(count);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceCount().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (disableTelemetryForThisProject) {
|
||||
return (
|
||||
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (serviceCount === 0) {
|
||||
return <TelemetryDocumentation telemetryType="traces" />;
|
||||
}
|
||||
|
||||
return <TraceTable />;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,20 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "Documentation",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.TRACES_DOCUMENTATION] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Book,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,11 +9,9 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import ExceptionsUnresolved from "../Pages/Exceptions/Unresolved";
|
||||
|
||||
import ExceptionsResolved from "../Pages/Exceptions/Resolved";
|
||||
|
||||
import ExceptionsArchived from "../Pages/Exceptions/Archived";
|
||||
|
||||
import ExceptionsDocumentationPage from "../Pages/Exceptions/Documentation";
|
||||
import ExceptionView from "../Pages/Exceptions/View/Index";
|
||||
|
||||
const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
|
||||
@@ -61,6 +59,15 @@ const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION] || ""}
|
||||
element={
|
||||
<ExceptionsDocumentationPage
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
{/* Exception View - separate from main layout */}
|
||||
|
||||
179
App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx
Normal file
179
App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import ComponentProps from "../Pages/PageComponentProps";
|
||||
import KubernetesLayout from "../Pages/Kubernetes/Layout";
|
||||
import KubernetesClusterViewLayout from "../Pages/Kubernetes/View/Layout";
|
||||
import PageMap from "../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil, KubernetesRoutePath } from "../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import KubernetesClusters from "../Pages/Kubernetes/Clusters";
|
||||
import KubernetesClusterView from "../Pages/Kubernetes/View/Index";
|
||||
import KubernetesClusterViewPods from "../Pages/Kubernetes/View/Pods";
|
||||
import KubernetesClusterViewPodDetail from "../Pages/Kubernetes/View/PodDetail";
|
||||
import KubernetesClusterViewNodes from "../Pages/Kubernetes/View/Nodes";
|
||||
import KubernetesClusterViewNodeDetail from "../Pages/Kubernetes/View/NodeDetail";
|
||||
import KubernetesClusterViewEvents from "../Pages/Kubernetes/View/Events";
|
||||
import KubernetesClusterViewControlPlane from "../Pages/Kubernetes/View/ControlPlane";
|
||||
import KubernetesClusterViewDelete from "../Pages/Kubernetes/View/Delete";
|
||||
import KubernetesClusterViewDocumentation from "../Pages/Kubernetes/View/Documentation";
|
||||
import KubernetesDocumentation from "../Pages/Kubernetes/Documentation";
|
||||
|
||||
const KubernetesRoutes: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Routes>
|
||||
<PageRoute path="/" element={<KubernetesLayout {...props} />}>
|
||||
<PageRoute
|
||||
path=""
|
||||
element={
|
||||
<KubernetesClusters
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_DOCUMENTATION)}
|
||||
element={
|
||||
<KubernetesDocumentation
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_DOCUMENTATION] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute
|
||||
path={KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW] || ""}
|
||||
element={<KubernetesClusterViewLayout {...props} />}
|
||||
>
|
||||
<PageRoute
|
||||
index
|
||||
element={
|
||||
<KubernetesClusterView
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_PODS,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewPods
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewPodDetail
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_NODES,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewNodes
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewNodeDetail
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewEvents
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewControlPlane
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_DELETE,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewDelete
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION,
|
||||
)}
|
||||
element={
|
||||
<KubernetesClusterViewDocumentation
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesRoutes;
|
||||
@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import MetricsPage from "../Pages/Metrics/Index";
|
||||
import MetricsDocumentationPage from "../Pages/Metrics/Documentation";
|
||||
|
||||
import MetricViewPage from "../Pages/Metrics/View/Index";
|
||||
|
||||
@@ -27,6 +28,15 @@ const MetricsRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={MetricsRoutePath[PageMap.METRICS_DOCUMENTATION] || ""}
|
||||
element={
|
||||
<MetricsDocumentationPage
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.METRICS_DOCUMENTATION] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
{/* Metric View */}
|
||||
|
||||
@@ -64,6 +64,7 @@ import SettingsLogPipelineView from "../Pages/Settings/LogPipelineView";
|
||||
import SettingsLogDropFilters from "../Pages/Settings/LogDropFilters";
|
||||
import SettingsLogDropFilterView from "../Pages/Settings/LogDropFilterView";
|
||||
import SettingsLogScrubRules from "../Pages/Settings/LogScrubRules";
|
||||
import SettingsMobileApps from "../Pages/Settings/MobileApps";
|
||||
|
||||
const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
@@ -92,6 +93,15 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_MOBILE_APPS)}
|
||||
element={
|
||||
<SettingsMobileApps
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_MOBILE_APPS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_AI_LOGS)}
|
||||
element={
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import TracesPage from "../Pages/Traces/Index";
|
||||
import TracesDocumentationPage from "../Pages/Traces/Documentation";
|
||||
|
||||
import TraceViewPage from "../Pages/Traces/View/Index";
|
||||
|
||||
@@ -27,6 +28,15 @@ const TracesRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={TracesRoutePath[PageMap.TRACES_DOCUMENTATION] || ""}
|
||||
element={
|
||||
<TracesDocumentationPage
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.TRACES_DOCUMENTATION] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
{/* Trace View */}
|
||||
|
||||
@@ -31,6 +31,11 @@ export function getExceptionsBreadcrumbs(
|
||||
"Exceptions",
|
||||
"View Exception",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Exceptions",
|
||||
"Documentation",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import PageMap from "../PageMap";
|
||||
import { BuildBreadcrumbLinksByTitles } from "./Helper";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import Link from "Common/Types/Link";
|
||||
|
||||
export function getKubernetesBreadcrumbs(
|
||||
path: string,
|
||||
): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTERS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"Clusters",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_PODS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Pods",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL,
|
||||
["Project", "Kubernetes", "View Cluster", "Pods", "Pod Detail"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_NODES, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Nodes",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL,
|
||||
["Project", "Kubernetes", "View Cluster", "Nodes", "Node Detail"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Events",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE,
|
||||
["Project", "Kubernetes", "View Cluster", "Control Plane"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_DELETE, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Delete Cluster",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION,
|
||||
["Project", "Kubernetes", "View Cluster", "Documentation"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"Documentation",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
@@ -11,6 +11,11 @@ export function getMetricsBreadcrumbs(path: string): Array<Link> | undefined {
|
||||
"Metrics",
|
||||
"Metrics Explorer",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.METRICS_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Metrics",
|
||||
"Documentation",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ export function getTracesBreadcrumbs(path: string): Array<Link> | undefined {
|
||||
"Traces",
|
||||
"Trace Explorer",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.TRACES_DOCUMENTATION, [
|
||||
"Project",
|
||||
"Traces",
|
||||
"Documentation",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -16,11 +16,13 @@ enum PageMap {
|
||||
METRICS_ROOT = "METRICS_ROOT",
|
||||
METRICS = "METRICS",
|
||||
METRIC_VIEW = "METRIC_VIEW",
|
||||
METRICS_DOCUMENTATION = "METRICS_DOCUMENTATION",
|
||||
|
||||
// Traces (standalone product)
|
||||
TRACES_ROOT = "TRACES_ROOT",
|
||||
TRACES = "TRACES",
|
||||
TRACE_VIEW = "TRACE_VIEW",
|
||||
TRACES_DOCUMENTATION = "TRACES_DOCUMENTATION",
|
||||
|
||||
HOME = "HOME",
|
||||
HOME_NOT_OPERATIONAL_MONITORS = "HOME_NOT_OPERATIONAL_MONITORS",
|
||||
@@ -214,6 +216,21 @@ enum PageMap {
|
||||
SERVICE_VIEW_CODE_REPOSITORIES = "SERVICE_VIEW_CODE_REPOSITORIES",
|
||||
SERVICE_DEPENDENCY_GRAPH = "SERVICE_DEPENDENCY_GRAPH",
|
||||
|
||||
// Kubernetes (standalone product)
|
||||
KUBERNETES_ROOT = "KUBERNETES_ROOT",
|
||||
KUBERNETES_CLUSTERS = "KUBERNETES_CLUSTERS",
|
||||
KUBERNETES_CLUSTER_VIEW = "KUBERNETES_CLUSTER_VIEW",
|
||||
KUBERNETES_CLUSTER_VIEW_PODS = "KUBERNETES_CLUSTER_VIEW_PODS",
|
||||
KUBERNETES_CLUSTER_VIEW_POD_DETAIL = "KUBERNETES_CLUSTER_VIEW_POD_DETAIL",
|
||||
KUBERNETES_CLUSTER_VIEW_NODES = "KUBERNETES_CLUSTER_VIEW_NODES",
|
||||
KUBERNETES_CLUSTER_VIEW_NODE_DETAIL = "KUBERNETES_CLUSTER_VIEW_NODE_DETAIL",
|
||||
KUBERNETES_CLUSTER_VIEW_EVENTS = "KUBERNETES_CLUSTER_VIEW_EVENTS",
|
||||
KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE = "KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE",
|
||||
KUBERNETES_CLUSTER_VIEW_DELETE = "KUBERNETES_CLUSTER_VIEW_DELETE",
|
||||
KUBERNETES_CLUSTER_VIEW_SETTINGS = "KUBERNETES_CLUSTER_VIEW_SETTINGS",
|
||||
KUBERNETES_CLUSTER_VIEW_DOCUMENTATION = "KUBERNETES_CLUSTER_VIEW_DOCUMENTATION",
|
||||
KUBERNETES_DOCUMENTATION = "KUBERNETES_DOCUMENTATION",
|
||||
|
||||
// Code Repository
|
||||
CODE_REPOSITORY_ROOT = "CODE_REPOSITORY_ROOT",
|
||||
CODE_REPOSITORY = "CODE_REPOSITORY",
|
||||
@@ -432,6 +449,9 @@ enum PageMap {
|
||||
|
||||
SETTINGS_NOTIFICATION_LOGS = "SETTINGS_NOTIFICATION_LOGS",
|
||||
|
||||
// Mobile Apps
|
||||
SETTINGS_MOBILE_APPS = "SETTINGS_MOBILE_APPS",
|
||||
|
||||
// AI Logs
|
||||
SETTINGS_AI_LOGS = "SETTINGS_AI_LOGS",
|
||||
|
||||
@@ -451,6 +471,7 @@ enum PageMap {
|
||||
EXCEPTIONS_ARCHIVED = "EXCEPTIONS_ARCHIVED",
|
||||
EXCEPTIONS_VIEW_ROOT = "EXCEPTIONS_VIEW_ROOT",
|
||||
EXCEPTIONS_VIEW = "EXCEPTIONS_VIEW",
|
||||
EXCEPTIONS_DOCUMENTATION = "EXCEPTIONS_DOCUMENTATION",
|
||||
|
||||
// Push Logs in resource views
|
||||
}
|
||||
|
||||
@@ -59,6 +59,20 @@ export const CodeRepositoryRoutePath: Dictionary<string> = {
|
||||
[PageMap.CODE_REPOSITORY_VIEW_SERVICES]: `${RouteParams.ModelID}/services`,
|
||||
};
|
||||
|
||||
export const KubernetesRoutePath: Dictionary<string> = {
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW]: `${RouteParams.ModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: `${RouteParams.ModelID}/pods`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: `${RouteParams.ModelID}/pods/${RouteParams.SubModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: `${RouteParams.ModelID}/nodes`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: `${RouteParams.ModelID}/nodes/${RouteParams.SubModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: `${RouteParams.ModelID}/events`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: `${RouteParams.ModelID}/control-plane`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]: `${RouteParams.ModelID}/documentation`,
|
||||
[PageMap.KUBERNETES_DOCUMENTATION]: `documentation`,
|
||||
};
|
||||
|
||||
export const WorkflowRoutePath: Dictionary<string> = {
|
||||
[PageMap.WORKFLOWS_LOGS]: "logs",
|
||||
[PageMap.WORKFLOWS_VARIABLES]: "variables",
|
||||
@@ -86,12 +100,14 @@ export const LogsRoutePath: Dictionary<string> = {
|
||||
export const MetricsRoutePath: Dictionary<string> = {
|
||||
[PageMap.METRICS]: "",
|
||||
[PageMap.METRIC_VIEW]: "view",
|
||||
[PageMap.METRICS_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
|
||||
// Traces product routes
|
||||
export const TracesRoutePath: Dictionary<string> = {
|
||||
[PageMap.TRACES]: "",
|
||||
[PageMap.TRACE_VIEW]: `view/${RouteParams.ModelID}`,
|
||||
[PageMap.TRACES_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
|
||||
export const ExceptionsRoutePath: Dictionary<string> = {
|
||||
@@ -101,6 +117,7 @@ export const ExceptionsRoutePath: Dictionary<string> = {
|
||||
[PageMap.EXCEPTIONS_ARCHIVED]: "archived",
|
||||
[PageMap.EXCEPTIONS_VIEW_ROOT]: "",
|
||||
[PageMap.EXCEPTIONS_VIEW]: `${RouteParams.ModelID}`,
|
||||
[PageMap.EXCEPTIONS_DOCUMENTATION]: "documentation",
|
||||
};
|
||||
|
||||
export const DashboardsRoutePath: Dictionary<string> = {
|
||||
@@ -299,6 +316,7 @@ export const SettingsRoutePath: Dictionary<string> = {
|
||||
[PageMap.SETTINGS_DANGERZONE]: "danger-zone",
|
||||
[PageMap.SETTINGS_NOTIFICATION_SETTINGS]: "notification-settings",
|
||||
[PageMap.SETTINGS_NOTIFICATION_LOGS]: "notification-logs",
|
||||
[PageMap.SETTINGS_MOBILE_APPS]: "mobile-apps",
|
||||
[PageMap.SETTINGS_AI_LOGS]: "ai-logs",
|
||||
[PageMap.SETTINGS_APIKEYS]: `api-keys`,
|
||||
[PageMap.SETTINGS_APIKEY_VIEW]: `api-keys/${RouteParams.ModelID}`,
|
||||
@@ -1465,6 +1483,82 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
// Kubernetes
|
||||
|
||||
[PageMap.KUBERNETES_ROOT]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/*`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTERS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DOCUMENTATION]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_DOCUMENTATION]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_DOCUMENTATION]
|
||||
}`,
|
||||
),
|
||||
|
||||
// Dashboards
|
||||
|
||||
[PageMap.DASHBOARDS_ROOT]: new Route(
|
||||
@@ -1981,6 +2075,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.METRICS_DOCUMENTATION]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/metrics/${
|
||||
MetricsRoutePath[PageMap.METRICS_DOCUMENTATION]
|
||||
}`,
|
||||
),
|
||||
|
||||
// Traces Product Routes
|
||||
[PageMap.TRACES_ROOT]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/traces/*`,
|
||||
@@ -1994,6 +2094,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.TRACES_DOCUMENTATION]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/traces/${
|
||||
TracesRoutePath[PageMap.TRACES_DOCUMENTATION]
|
||||
}`,
|
||||
),
|
||||
|
||||
// User Settings Routes
|
||||
[PageMap.USER_SETTINGS_ROOT]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/user-settings/*`,
|
||||
@@ -2105,6 +2211,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SETTINGS_MOBILE_APPS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/settings/${
|
||||
SettingsRoutePath[PageMap.SETTINGS_MOBILE_APPS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SETTINGS_AI_LOGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/settings/${
|
||||
SettingsRoutePath[PageMap.SETTINGS_AI_LOGS]
|
||||
@@ -2468,6 +2580,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
ExceptionsRoutePath[PageMap.EXCEPTIONS_VIEW]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.EXCEPTIONS_DOCUMENTATION]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/exceptions/${
|
||||
ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION]
|
||||
}`,
|
||||
),
|
||||
};
|
||||
|
||||
export class RouteUtil {
|
||||
|
||||
@@ -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 } } = {};
|
||||
|
||||
640
Common/Models/DatabaseModels/KubernetesCluster.ts
Normal file
640
Common/Models/DatabaseModels/KubernetesCluster.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1773761409952 implements MigrationInterface {
|
||||
public name = "MigrationName1773761409952";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_5ae5bbb0c93c048b0b76b1d426" ON "KubernetesCluster" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b9259f6741a7965a518e258f61" ON "KubernetesCluster" ("clusterIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ed1b53bd041aa21b44ca8cdab5" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2ec82ad068e84cf762c32ad7c7" ON "KubernetesClusterLabel" ("labelId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_1bee392c44b1aebe754932133a8" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_1bee392c44b1aebe754932133a8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2ec82ad068e84cf762c32ad7c7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ed1b53bd041aa21b44ca8cdab5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b9259f6741a7965a518e258f61"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_5ae5bbb0c93c048b0b76b1d426"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"`);
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,8 @@ import { AddLogSavedView1772355000000 } from "./1772355000000-AddLogSavedView";
|
||||
import { MigrationName1773344537755 } from "./1773344537755-MigrationName";
|
||||
import { MigrationName1773402621107 } from "./1773402621107-MigrationName";
|
||||
import { MigrationName1773676206197 } from "./1773676206197-MigrationName";
|
||||
import { MigrationName1774000000000 } from "./1774000000000-MigrationName";
|
||||
import { MigrationName1773761409952 } from "./1773761409952-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -536,4 +538,6 @@ export default [
|
||||
MigrationName1773344537755,
|
||||
MigrationName1773402621107,
|
||||
MigrationName1773676206197,
|
||||
MigrationName1774000000000,
|
||||
MigrationName1773761409952,
|
||||
];
|
||||
|
||||
109
Common/Server/Services/KubernetesClusterService.ts
Normal file
109
Common/Server/Services/KubernetesClusterService.ts
Normal 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();
|
||||
@@ -37,6 +37,7 @@ import Typeof from "../../Types/Typeof";
|
||||
import CookieParser from "cookie-parser";
|
||||
import cors from "cors";
|
||||
import zlib from "zlib";
|
||||
import path from "path";
|
||||
import "ejs";
|
||||
// Make sure we have stack trace for debugging.
|
||||
Error.stackTraceLimit = Infinity;
|
||||
@@ -240,12 +241,15 @@ const init: InitFunction = async (
|
||||
},
|
||||
);
|
||||
|
||||
app.use(`/${appName}`, ExpressStatic("/usr/src/app/public"));
|
||||
app.use(
|
||||
`/${appName}`,
|
||||
ExpressStatic(path.resolve(process.cwd(), "public")),
|
||||
);
|
||||
|
||||
app.get(
|
||||
`/${appName}/dist/Index.js`,
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.sendFile("/usr/src/app/public/dist/Index.js");
|
||||
res.sendFile(path.resolve(process.cwd(), "public/dist/Index.js"));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -285,7 +289,7 @@ const init: InitFunction = async (
|
||||
return;
|
||||
}
|
||||
|
||||
return res.render("/usr/src/app/views/index.ejs", {
|
||||
return res.render(path.resolve(process.cwd(), "views/index.ejs"), {
|
||||
enableGoogleTagManager: IsBillingEnabled || false,
|
||||
...variables,
|
||||
});
|
||||
|
||||
@@ -320,6 +320,7 @@ enum IconProp {
|
||||
TableCellsIcon = "TableCellsIcon",
|
||||
UserIcon = "UserIcon",
|
||||
XCircle = "XCircle",
|
||||
Kubernetes = "Kubernetes",
|
||||
}
|
||||
|
||||
export default IconProp;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2733,6 +2733,57 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="m15 11.25 1.5 1.5.75-.75V8.758l2.276-.61a3 3 0 1 0-3.675-3.675l-.61 2.277H12l-.75.75 1.5 1.5M15 11.25l-8.47 8.47c-.34.34-.8.53-1.28.53s-.94-.19-1.28-.53a1.818 1.818 0 0 1 0-2.56l8.47-8.47M15 11.25 12 8.25"
|
||||
/>,
|
||||
);
|
||||
} else if (icon === IconProp.Kubernetes) {
|
||||
// Kubernetes helm wheel — 7-sided shape with 7 spokes, matching the official logo
|
||||
const cx: number = 12;
|
||||
const cy: number = 12;
|
||||
const outerR: number = 9.5;
|
||||
const innerR: number = 2.2;
|
||||
const spokeEnd: number = 8;
|
||||
const sides: number = 7;
|
||||
const offsetAngle: number = -Math.PI / 2; // start from top
|
||||
|
||||
const outerPoints: string[] = [];
|
||||
const spokes: React.ReactElement[] = [];
|
||||
|
||||
for (let i: number = 0; i < sides; i++) {
|
||||
const angle: number = offsetAngle + (2 * Math.PI * i) / sides;
|
||||
const ox: number = cx + outerR * Math.cos(angle);
|
||||
const oy: number = cy + outerR * Math.sin(angle);
|
||||
outerPoints.push(`${ox.toFixed(2)},${oy.toFixed(2)}`);
|
||||
|
||||
const sx: number = cx + innerR * Math.cos(angle);
|
||||
const sy: number = cy + innerR * Math.sin(angle);
|
||||
const ex: number = cx + spokeEnd * Math.cos(angle);
|
||||
const ey: number = cy + spokeEnd * Math.sin(angle);
|
||||
|
||||
spokes.push(
|
||||
<line
|
||||
key={`spoke-${i}`}
|
||||
x1={sx.toFixed(2)}
|
||||
y1={sy.toFixed(2)}
|
||||
x2={ex.toFixed(2)}
|
||||
y2={ey.toFixed(2)}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return getSvgWrapper(
|
||||
<>
|
||||
<polygon
|
||||
points={outerPoints.join(" ")}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx={cx} cy={cy} r={innerR} />
|
||||
{spokes}
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface ComponentProps {
|
||||
onEditSavedView?: ((viewId: string) => void) | undefined;
|
||||
onDeleteSavedView?: ((viewId: string) => void) | undefined;
|
||||
onUpdateCurrentSavedView?: (() => void) | undefined;
|
||||
onShowDocumentation?: (() => void) | undefined;
|
||||
viewMode?: LogsViewMode | undefined;
|
||||
onViewModeChange?: ((mode: LogsViewMode) => void) | undefined;
|
||||
analyticsServiceIds?: Array<string> | undefined;
|
||||
@@ -805,6 +806,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
||||
return !prev;
|
||||
});
|
||||
},
|
||||
onShowDocumentation: props.onShowDocumentation,
|
||||
};
|
||||
|
||||
const showSidebar: boolean =
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface LogsViewerToolbarProps {
|
||||
onExportJSON?: (() => void) | undefined;
|
||||
showKeyboardShortcuts?: boolean | undefined;
|
||||
onToggleKeyboardShortcuts?: (() => void) | undefined;
|
||||
onShowDocumentation?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
||||
@@ -155,6 +156,30 @@ const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.onShowDocumentation && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50"
|
||||
onClick={props.onShowDocumentation}
|
||||
title="Setup Documentation"
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
Docs
|
||||
</button>
|
||||
)}
|
||||
|
||||
{props.onToggleKeyboardShortcuts && (
|
||||
<div className="relative">
|
||||
<button
|
||||
|
||||
@@ -208,9 +208,9 @@ const CodeBlock: FunctionComponent<{
|
||||
(language ? language.charAt(0).toUpperCase() + language.slice(1) : "");
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg mt-4 mb-4 overflow-hidden border border-gray-700">
|
||||
<div className="relative rounded-lg mt-3 mb-3 overflow-hidden border border-gray-200 shadow-sm">
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-800/60 border-b border-gray-700/60">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-800 border-b border-gray-700">
|
||||
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-400 select-none">
|
||||
{displayLang}
|
||||
</span>
|
||||
@@ -261,7 +261,7 @@ const CodeBlock: FunctionComponent<{
|
||||
children={content}
|
||||
language={language}
|
||||
style={vscDarkPlus}
|
||||
className="!rounded-none !mt-0 !mb-0 !bg-gray-900 !pt-6 !pb-4 !px-4 text-sm !border-0"
|
||||
className="!rounded-none !mt-0 !mb-0 !bg-gray-900 !pt-3 !pb-3 !px-4 text-sm !border-0"
|
||||
codeTagProps={{ className: "font-mono" }}
|
||||
/>
|
||||
</div>
|
||||
@@ -279,7 +279,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h1: ({ ...props }: any) => {
|
||||
return (
|
||||
<h1
|
||||
className="text-4xl mt-8 mb-6 border-b-2 border-blue-500 pb-2 text-gray-900 font-bold"
|
||||
className="text-lg mt-6 mb-3 text-gray-900 font-bold"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -287,7 +287,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h2: ({ ...props }: any) => {
|
||||
return (
|
||||
<h2
|
||||
className="text-3xl mt-6 mb-4 border-b border-gray-300 pb-1 text-gray-900 font-semibold"
|
||||
className="text-base mt-6 mb-2 text-gray-900 font-semibold"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -295,7 +295,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h3: ({ ...props }: any) => {
|
||||
return (
|
||||
<h3
|
||||
className="text-2xl mt-6 mb-3 text-gray-900 font-semibold"
|
||||
className="text-base mt-4 mb-2 text-gray-900 font-semibold"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -303,7 +303,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h4: ({ ...props }: any) => {
|
||||
return (
|
||||
<h4
|
||||
className="text-xl mt-5 mb-3 text-gray-900 font-medium"
|
||||
className="text-sm mt-3 mb-2 text-gray-900 font-semibold"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -311,7 +311,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h5: ({ ...props }: any) => {
|
||||
return (
|
||||
<h5
|
||||
className="text-lg mt-4 mb-2 text-gray-900 font-medium"
|
||||
className="text-sm mt-3 mb-1 text-gray-900 font-medium"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -319,7 +319,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
h6: ({ ...props }: any) => {
|
||||
return (
|
||||
<h6
|
||||
className="text-base mt-3 mb-2 text-gray-900 font-medium"
|
||||
className="text-sm mt-2 mb-1 text-gray-700 font-medium"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -327,7 +327,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
p: ({ ...props }: any) => {
|
||||
return (
|
||||
<p
|
||||
className="text-base mt-3 mb-4 text-gray-700 leading-relaxed"
|
||||
className="text-sm mt-2 mb-3 text-gray-700 leading-relaxed"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -352,24 +352,24 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Avoid double borders when SyntaxHighlighter is already styling the block.
|
||||
const isSyntaxHighlighter: boolean =
|
||||
/*
|
||||
* If the child is a custom component (CodeBlock, MermaidDiagram, etc.)
|
||||
* rather than a plain HTML element like <code>, skip pre styling.
|
||||
* Checking typeof type !== "string" is minification-safe unlike checking type.name.
|
||||
*/
|
||||
const isCustomComponent: boolean =
|
||||
React.isValidElement(children) &&
|
||||
// name can be 'SyntaxHighlighter' or wrapped/minified; fall back to presence of 'children' prop with 'react-syntax-highlighter' data attribute.
|
||||
(((children as any).type &&
|
||||
((children as any).type.name === "SyntaxHighlighter" ||
|
||||
(children as any).type.displayName ===
|
||||
"SyntaxHighlighter")) ||
|
||||
(children as any).props?.className?.includes(
|
||||
"syntax-highlighter",
|
||||
));
|
||||
typeof (children as any).type !== "string";
|
||||
|
||||
const baseClass: string = isSyntaxHighlighter
|
||||
? "mt-4 mb-4 rounded-lg overflow-hidden"
|
||||
: "bg-gray-900 text-gray-100 mt-4 mb-4 p-2 rounded-lg text-sm overflow-x-auto border border-gray-700";
|
||||
if (isCustomComponent) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className={baseClass} {...rest}>
|
||||
<pre
|
||||
className="bg-gray-900 text-gray-100 mt-3 mb-3 p-3 rounded-md text-sm overflow-x-auto border border-gray-700"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
@@ -377,7 +377,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
strong: ({ ...props }: any) => {
|
||||
return (
|
||||
<strong
|
||||
className="text-base font-semibold text-gray-900"
|
||||
className="text-sm font-semibold text-gray-900"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -385,56 +385,76 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
li: ({ ...props }: any) => {
|
||||
return (
|
||||
<li
|
||||
className="text-base mt-2 mb-1 text-gray-700 leading-relaxed"
|
||||
className="text-sm mt-1 mb-1 text-gray-700 leading-relaxed"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
ul: ({ ...props }: any) => {
|
||||
return <ul className="list-disc pl-8 mt-2 mb-4" {...props} />;
|
||||
return <ul className="list-disc pl-6 mt-1 mb-3" {...props} />;
|
||||
},
|
||||
ol: ({ ...props }: any) => {
|
||||
return <ol className="list-decimal pl-8 mt-2 mb-4" {...props} />;
|
||||
return <ol className="list-decimal pl-6 mt-1 mb-3" {...props} />;
|
||||
},
|
||||
blockquote: ({ ...props }: any) => {
|
||||
blockquote: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<blockquote
|
||||
className="border-l-4 border-blue-500 pl-4 italic text-gray-600 bg-gray-50 py-2 my-4"
|
||||
className="rounded-lg border border-amber-200 bg-amber-50/50 my-4 not-italic overflow-hidden"
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<svg
|
||||
className="h-5 w-5 flex-shrink-0 text-amber-500 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="text-sm text-gray-700 leading-relaxed [&>p]:mt-0 [&>p]:mb-0 [&>p>strong:first-child]:text-amber-700 [&>p>strong:first-child]:mr-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</blockquote>
|
||||
);
|
||||
},
|
||||
table: ({ ...props }: any) => {
|
||||
return (
|
||||
<table
|
||||
className="min-w-full table-auto border-collapse border border-gray-300 mt-4 mb-4"
|
||||
{...props}
|
||||
/>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 mt-4 mb-4 shadow-sm">
|
||||
<table
|
||||
className="min-w-full table-auto border-collapse text-sm"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
thead: ({ ...props }: any) => {
|
||||
return <thead className="bg-gray-100" {...props} />;
|
||||
return <thead className="bg-gray-50" {...props} />;
|
||||
},
|
||||
tbody: ({ ...props }: any) => {
|
||||
return <tbody {...props} />;
|
||||
return <tbody className="divide-y divide-gray-100" {...props} />;
|
||||
},
|
||||
tr: ({ ...props }: any) => {
|
||||
return <tr className="border-b border-gray-200" {...props} />;
|
||||
return (
|
||||
<tr className="hover:bg-gray-50 transition-colors" {...props} />
|
||||
);
|
||||
},
|
||||
th: ({ ...props }: any) => {
|
||||
return (
|
||||
<th
|
||||
className="px-4 py-2 text-left text-sm font-semibold text-gray-900 border border-gray-300"
|
||||
className="px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 border-b border-gray-200"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
td: ({ ...props }: any) => {
|
||||
return (
|
||||
<td
|
||||
className="px-4 py-2 text-sm text-gray-700 border border-gray-300"
|
||||
{...props}
|
||||
/>
|
||||
<td className="px-4 py-2.5 text-sm text-gray-700" {...props} />
|
||||
);
|
||||
},
|
||||
hr: ({ ...props }: any) => {
|
||||
@@ -457,19 +477,31 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
return <MermaidDiagram chart={content} />;
|
||||
}
|
||||
|
||||
const codeClassName: string =
|
||||
content.includes("\n") ||
|
||||
(match &&
|
||||
const isMultiline: boolean = content.includes("\n");
|
||||
const hasLanguage: boolean = Boolean(
|
||||
match &&
|
||||
match?.filter((item: string) => {
|
||||
return item.includes("language-");
|
||||
}).length > 0)
|
||||
? ""
|
||||
: "text-sm px-2 py-1 bg-gray-200 rounded text-gray-900 font-mono";
|
||||
}).length > 0,
|
||||
);
|
||||
|
||||
return match ? (
|
||||
<CodeBlock language={match[1]!} content={content} rest={rest} />
|
||||
) : (
|
||||
<code className={codeClassName} {...rest}>
|
||||
// Multiline code blocks (with or without language) get the full CodeBlock treatment
|
||||
if (hasLanguage || isMultiline) {
|
||||
return (
|
||||
<CodeBlock
|
||||
language={match ? match[1]! : "text"}
|
||||
content={content}
|
||||
rest={rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline code
|
||||
return (
|
||||
<code
|
||||
className="text-xs px-1.5 py-0.5 bg-gray-100 border border-gray-200 rounded text-gray-800 font-mono"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 z-10 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
|
||||
<div className="absolute left-0 z-50 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
|
||||
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
|
||||
{/* Sections */}
|
||||
<div className="p-6">
|
||||
@@ -127,7 +127,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute left-1/2 z-10 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
|
||||
className={`absolute left-1/2 z-50 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
|
||||
>
|
||||
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
|
||||
{/* Menu Items */}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Critical Path Analysis for distributed traces
|
||||
// Computes self-time, critical path, and bottleneck identification
|
||||
/*
|
||||
* Critical Path Analysis for distributed traces
|
||||
* Computes self-time, critical path, and bottleneck identification
|
||||
*/
|
||||
|
||||
export interface SpanData {
|
||||
spanId: string;
|
||||
@@ -71,9 +73,10 @@ export default class CriticalPathUtil {
|
||||
selfTimeUnixNano,
|
||||
childTimeUnixNano,
|
||||
totalTimeUnixNano: span.durationUnixNano,
|
||||
selfTimePercent: span.durationUnixNano > 0
|
||||
? (selfTimeUnixNano / span.durationUnixNano) * 100
|
||||
: 0,
|
||||
selfTimePercent:
|
||||
span.durationUnixNano > 0
|
||||
? (selfTimeUnixNano / span.durationUnixNano) * 100
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,9 +113,14 @@ export default class CriticalPathUtil {
|
||||
}
|
||||
|
||||
// Sort by start time
|
||||
intervals.sort((a: { start: number; end: number }, b: { start: number; end: number }) => {
|
||||
return a.start - b.start;
|
||||
});
|
||||
intervals.sort(
|
||||
(
|
||||
a: { start: number; end: number },
|
||||
b: { start: number; end: number },
|
||||
) => {
|
||||
return a.start - b.start;
|
||||
},
|
||||
);
|
||||
|
||||
// Merge overlapping intervals
|
||||
let mergedDuration: number = 0;
|
||||
@@ -198,9 +206,10 @@ export default class CriticalPathUtil {
|
||||
const criticalPathCache: Map<string, { weight: number; path: string[] }> =
|
||||
new Map();
|
||||
|
||||
const computeWeight = (
|
||||
spanId: string,
|
||||
): { weight: number; path: string[] } => {
|
||||
const computeWeight: (spanId: string) => {
|
||||
weight: number;
|
||||
path: string[];
|
||||
} = (spanId: string): { weight: number; path: string[] } => {
|
||||
const cached: { weight: number; path: string[] } | undefined =
|
||||
criticalPathCache.get(spanId);
|
||||
if (cached) {
|
||||
@@ -275,9 +284,7 @@ export default class CriticalPathUtil {
|
||||
/**
|
||||
* Compute latency breakdown by service.
|
||||
*/
|
||||
public static computeServiceBreakdown(
|
||||
spans: SpanData[],
|
||||
): ServiceBreakdown[] {
|
||||
public static computeServiceBreakdown(spans: SpanData[]): ServiceBreakdown[] {
|
||||
const selfTimes: Map<string, SpanSelfTime> =
|
||||
CriticalPathUtil.computeSelfTimes(spans);
|
||||
|
||||
@@ -302,12 +309,15 @@ export default class CriticalPathUtil {
|
||||
|
||||
for (const span of spans) {
|
||||
const serviceId: string = span.serviceId || "unknown";
|
||||
const entry: { totalDuration: number; selfTime: number; spanCount: number } =
|
||||
serviceMap.get(serviceId) || {
|
||||
totalDuration: 0,
|
||||
selfTime: 0,
|
||||
spanCount: 0,
|
||||
};
|
||||
const entry: {
|
||||
totalDuration: number;
|
||||
selfTime: number;
|
||||
spanCount: number;
|
||||
} = serviceMap.get(serviceId) || {
|
||||
totalDuration: 0,
|
||||
selfTime: 0,
|
||||
spanCount: 0,
|
||||
};
|
||||
|
||||
entry.totalDuration += span.durationUnixNano;
|
||||
const selfTime: SpanSelfTime | undefined = selfTimes.get(span.spanId);
|
||||
@@ -329,11 +339,9 @@ export default class CriticalPathUtil {
|
||||
}
|
||||
|
||||
// Sort by self-time descending (biggest contributors first)
|
||||
result.sort(
|
||||
(a: ServiceBreakdown, b: ServiceBreakdown) => {
|
||||
return b.selfTimeUnixNano - a.selfTimeUnixNano;
|
||||
},
|
||||
);
|
||||
result.sort((a: ServiceBreakdown, b: ServiceBreakdown) => {
|
||||
return b.selfTimeUnixNano - a.selfTimeUnixNano;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
18
HelmChart/Public/kubernetes-agent/.helmignore
Normal file
18
HelmChart/Public/kubernetes-agent/.helmignore
Normal 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/
|
||||
15
HelmChart/Public/kubernetes-agent/Chart.yaml
Normal file
15
HelmChart/Public/kubernetes-agent/Chart.yaml
Normal 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"
|
||||
24
HelmChart/Public/kubernetes-agent/templates/NOTES.txt
Normal file
24
HelmChart/Public/kubernetes-agent/templates/NOTES.txt
Normal 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
|
||||
59
HelmChart/Public/kubernetes-agent/templates/_helpers.tpl
Normal file
59
HelmChart/Public/kubernetes-agent/templates/_helpers.tpl
Normal 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 }}
|
||||
@@ -0,0 +1,140 @@
|
||||
{{- if .Values.logs.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-daemonset
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
data:
|
||||
otel-collector-config.yaml: |
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: "0.0.0.0:13133"
|
||||
|
||||
receivers:
|
||||
# Collect pod logs from /var/log/pods
|
||||
filelog:
|
||||
include:
|
||||
- /var/log/pods/*/*/*.log
|
||||
exclude:
|
||||
# Exclude the agent's own logs to avoid feedback loop
|
||||
- /var/log/pods/{{ "{{ .Release.Namespace }}" }}_{{ "{{ include \"kubernetes-agent.fullname\" . }}" }}*/**/*.log
|
||||
start_at: end
|
||||
include_file_path: true
|
||||
include_file_name: false
|
||||
operators:
|
||||
# Parse CRI log format
|
||||
- type: router
|
||||
id: get-format
|
||||
routes:
|
||||
- output: parser-docker
|
||||
expr: 'body matches "^\\{"'
|
||||
- output: parser-cri
|
||||
expr: 'body matches "^[^ Z]+ "'
|
||||
- output: parser-containerd
|
||||
expr: 'body matches "^[^ Z]+Z"'
|
||||
# Docker JSON log format
|
||||
- type: json_parser
|
||||
id: parser-docker
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
# CRI log format
|
||||
- type: regex_parser
|
||||
id: parser-cri
|
||||
regex: '^(?P<time>[^ Z]+) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%L%j'
|
||||
# Containerd log format
|
||||
- type: regex_parser
|
||||
id: parser-containerd
|
||||
regex: '^(?P<time>[^ ^Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
# Extract k8s metadata from file path
|
||||
- type: regex_parser
|
||||
id: extract-metadata-from-filepath
|
||||
regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]+)\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log$'
|
||||
parse_from: attributes["log.file.path"]
|
||||
- type: move
|
||||
from: attributes.log
|
||||
to: body
|
||||
- type: move
|
||||
from: attributes.stream
|
||||
to: attributes["log.iostream"]
|
||||
- type: move
|
||||
from: attributes.namespace
|
||||
to: resource["k8s.namespace.name"]
|
||||
- type: move
|
||||
from: attributes.pod_name
|
||||
to: resource["k8s.pod.name"]
|
||||
- type: move
|
||||
from: attributes.container_name
|
||||
to: resource["k8s.container.name"]
|
||||
- type: move
|
||||
from: attributes.uid
|
||||
to: resource["k8s.pod.uid"]
|
||||
|
||||
processors:
|
||||
# Enrich with K8s metadata
|
||||
k8sattributes:
|
||||
auth_type: serviceAccount
|
||||
extract:
|
||||
metadata:
|
||||
- k8s.pod.name
|
||||
- k8s.pod.uid
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.replicaset.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.container.name
|
||||
pod_association:
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.uid
|
||||
|
||||
# Stamp with cluster name
|
||||
resource:
|
||||
attributes:
|
||||
- key: k8s.cluster.name
|
||||
value: {{ .Values.clusterName | quote }}
|
||||
action: upsert
|
||||
|
||||
batch:
|
||||
send_batch_size: 1024
|
||||
timeout: 10s
|
||||
|
||||
memory_limiter:
|
||||
check_interval: 5s
|
||||
limit_mib: 200
|
||||
spike_limit_mib: 50
|
||||
|
||||
exporters:
|
||||
otlphttp:
|
||||
endpoint: "{{ .Values.oneuptime.url }}/otlp"
|
||||
headers:
|
||||
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
|
||||
|
||||
service:
|
||||
extensions:
|
||||
- health_check
|
||||
pipelines:
|
||||
logs:
|
||||
receivers:
|
||||
- filelog
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
{{- end }}
|
||||
@@ -0,0 +1,183 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-deployment
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
data:
|
||||
otel-collector-config.yaml: |
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: "0.0.0.0:13133"
|
||||
|
||||
receivers:
|
||||
# Collect node, pod, and container resource metrics from kubelet
|
||||
kubeletstats:
|
||||
collection_interval: {{ .Values.collectionInterval }}
|
||||
auth_type: serviceAccount
|
||||
endpoint: "https://${env:NODE_NAME}:10250"
|
||||
insecure_skip_verify: true
|
||||
metric_groups:
|
||||
- node
|
||||
- pod
|
||||
- container
|
||||
extra_metadata_labels:
|
||||
- container.id
|
||||
k8s_api_config:
|
||||
auth_type: serviceAccount
|
||||
|
||||
# Collect cluster-level metrics from the Kubernetes API
|
||||
k8s_cluster:
|
||||
collection_interval: {{ .Values.collectionInterval }}
|
||||
node_conditions_to_report:
|
||||
- Ready
|
||||
- MemoryPressure
|
||||
- DiskPressure
|
||||
- PIDPressure
|
||||
- NetworkUnavailable
|
||||
allocatable_types_to_report:
|
||||
- cpu
|
||||
- memory
|
||||
- storage
|
||||
|
||||
# Watch Kubernetes events and ingest as logs
|
||||
k8sobjects:
|
||||
objects:
|
||||
- name: events
|
||||
mode: watch
|
||||
group: events.k8s.io
|
||||
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
# Scrape control plane metrics via Prometheus endpoints
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: etcd
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.etcd.insecureSkipVerify }}
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.etcd.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-apiserver
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.apiServer.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.apiServer.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-scheduler
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.scheduler.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.scheduler.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-controller-manager
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.controllerManager.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.controllerManager.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
processors:
|
||||
# Enrich all telemetry with Kubernetes metadata
|
||||
k8sattributes:
|
||||
auth_type: serviceAccount
|
||||
extract:
|
||||
metadata:
|
||||
- k8s.pod.name
|
||||
- k8s.pod.uid
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.replicaset.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.job.name
|
||||
- k8s.cronjob.name
|
||||
- k8s.container.name
|
||||
labels:
|
||||
- tag_name: k8s.pod.label.app
|
||||
key: app
|
||||
from: pod
|
||||
- tag_name: k8s.pod.label.app.kubernetes.io/name
|
||||
key: app.kubernetes.io/name
|
||||
from: pod
|
||||
pod_association:
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.ip
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.uid
|
||||
- sources:
|
||||
- from: connection
|
||||
|
||||
# Stamp all telemetry with the cluster name
|
||||
resource:
|
||||
attributes:
|
||||
- key: k8s.cluster.name
|
||||
value: {{ .Values.clusterName | quote }}
|
||||
action: upsert
|
||||
|
||||
# Batch telemetry for efficient export
|
||||
batch:
|
||||
send_batch_size: 200
|
||||
send_batch_max_size: 500
|
||||
timeout: 10s
|
||||
|
||||
# Limit memory usage
|
||||
memory_limiter:
|
||||
check_interval: 5s
|
||||
limit_mib: 1500
|
||||
spike_limit_mib: 300
|
||||
|
||||
exporters:
|
||||
otlphttp:
|
||||
endpoint: "{{ .Values.oneuptime.url }}/otlp"
|
||||
headers:
|
||||
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
|
||||
|
||||
service:
|
||||
extensions:
|
||||
- health_check
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers:
|
||||
- kubeletstats
|
||||
- k8s_cluster
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
- prometheus
|
||||
{{- end }}
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
logs:
|
||||
receivers:
|
||||
- k8sobjects
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
56
HelmChart/Public/kubernetes-agent/templates/daemonset.yaml
Normal file
56
HelmChart/Public/kubernetes-agent/templates/daemonset.yaml
Normal 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 }}
|
||||
67
HelmChart/Public/kubernetes-agent/templates/deployment.yaml
Normal file
67
HelmChart/Public/kubernetes-agent/templates/deployment.yaml
Normal 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
|
||||
88
HelmChart/Public/kubernetes-agent/templates/rbac.yaml
Normal file
88
HelmChart/Public/kubernetes-agent/templates/rbac.yaml
Normal 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 }}
|
||||
10
HelmChart/Public/kubernetes-agent/templates/secret.yaml
Normal file
10
HelmChart/Public/kubernetes-agent/templates/secret.yaml
Normal 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 }}
|
||||
210
HelmChart/Public/kubernetes-agent/values.schema.json
Normal file
210
HelmChart/Public/kubernetes-agent/values.schema.json
Normal file
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["oneuptime", "clusterName"],
|
||||
"properties": {
|
||||
"oneuptime": {
|
||||
"type": "object",
|
||||
"description": "OneUptime instance connection details",
|
||||
"required": ["url", "apiKey"],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of your OneUptime instance (e.g., https://oneuptime.example.com)"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "Project API key from OneUptime (Settings > API Keys)"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"clusterName": {
|
||||
"type": "string",
|
||||
"description": "Unique name for this cluster (used as k8s.cluster.name attribute)"
|
||||
},
|
||||
"namespaceFilters": {
|
||||
"type": "object",
|
||||
"description": "Namespace filters to limit which namespaces are monitored",
|
||||
"properties": {
|
||||
"include": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "If set, only these namespaces are monitored (empty = all namespaces)"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Namespaces to exclude from monitoring"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"description": "OTel Collector image configuration",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["Always", "IfNotPresent", "Never"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"deployment": {
|
||||
"type": "object",
|
||||
"description": "Deployment (metrics + events collector) resource configuration",
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": { "type": "string" },
|
||||
"memory": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": { "type": "string" },
|
||||
"memory": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"controlPlane": {
|
||||
"type": "object",
|
||||
"description": "Control plane monitoring configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"etcd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"insecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"apiServer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"insecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"scheduler": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"insecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"controllerManager": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"insecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"logs": {
|
||||
"type": "object",
|
||||
"description": "Pod log collection via DaemonSet with filelog receiver",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": { "type": "string" },
|
||||
"memory": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": { "type": "string" },
|
||||
"memory": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"collectionInterval": {
|
||||
"type": "string",
|
||||
"description": "Collection interval for metrics (e.g., 30s, 1m)"
|
||||
},
|
||||
"serviceAccount": {
|
||||
"type": "object",
|
||||
"description": "Service account configuration",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
80
HelmChart/Public/kubernetes-agent/values.yaml
Normal file
80
HelmChart/Public/kubernetes-agent/values.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
# OneUptime Kubernetes Agent Configuration
|
||||
|
||||
# Required: Your OneUptime instance connection details
|
||||
oneuptime:
|
||||
# URL of your OneUptime instance (e.g., https://oneuptime.example.com)
|
||||
url: ""
|
||||
# Project API key from OneUptime (Settings > API Keys)
|
||||
apiKey: ""
|
||||
|
||||
# Required: Unique name for this cluster (used as k8s.cluster.name attribute)
|
||||
clusterName: ""
|
||||
|
||||
# Namespace filters — limit which namespaces are monitored
|
||||
namespaceFilters:
|
||||
# If set, only these namespaces are monitored (empty = all namespaces)
|
||||
include: []
|
||||
# Namespaces to exclude from monitoring
|
||||
exclude:
|
||||
- kube-system
|
||||
|
||||
# OTel Collector image configuration
|
||||
image:
|
||||
repository: otel/opentelemetry-collector-contrib
|
||||
tag: "0.96.0"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Deployment (metrics + events collector) resource configuration
|
||||
deployment:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
# Control plane monitoring (etcd, API server, scheduler, controller manager)
|
||||
# Disabled by default — enable for self-managed clusters.
|
||||
# Managed K8s (EKS, GKE, AKS) typically don't expose control plane metrics.
|
||||
controlPlane:
|
||||
enabled: false
|
||||
etcd:
|
||||
# Endpoints to scrape etcd metrics from
|
||||
endpoints:
|
||||
- https://localhost:2379/metrics
|
||||
# TLS configuration for etcd
|
||||
insecureSkipVerify: true
|
||||
apiServer:
|
||||
endpoints:
|
||||
- https://localhost:6443/metrics
|
||||
insecureSkipVerify: true
|
||||
scheduler:
|
||||
endpoints:
|
||||
- https://localhost:10259/metrics
|
||||
insecureSkipVerify: true
|
||||
controllerManager:
|
||||
endpoints:
|
||||
- https://localhost:10257/metrics
|
||||
insecureSkipVerify: true
|
||||
|
||||
# Pod log collection via DaemonSet with filelog receiver
|
||||
logs:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
# Collection intervals
|
||||
collectionInterval: 30s
|
||||
|
||||
# Service account configuration
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: ""
|
||||
annotations: {}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if .Values.aiAgent.enabled }}
|
||||
{{- if and .Values.aiAgent.enabled (not .Values.deployment.disableDeployments) }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.app.enabled }}
|
||||
{{- if and $.Values.app.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime app Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -137,7 +137,7 @@ spec:
|
||||
---
|
||||
{{- end }}
|
||||
|
||||
{{- if $.Values.app.enabled }}
|
||||
{{- if and $.Values.app.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime app Service
|
||||
{{- $appPorts := dict "port" $.Values.app.ports.http -}}
|
||||
{{- $appServiceArgs := dict "ServiceName" "app" "Ports" $appPorts "Release" $.Release "Values" $.Values -}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.cronJobs.cleanup.enabled }}
|
||||
{{- if and $.Values.cronJobs.cleanup.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.home.enabled }}
|
||||
{{- if and $.Values.home.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime Home Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -126,7 +126,7 @@ spec:
|
||||
|
||||
{{- end }}
|
||||
|
||||
{{- if $.Values.home.enabled }}
|
||||
{{- if and $.Values.home.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime home Service
|
||||
{{- $homePorts := $.Values.home.ports -}}
|
||||
{{- $homeServiceArgs := dict "ServiceName" "home" "Ports" $homePorts "Release" $.Release "Values" $.Values -}}
|
||||
|
||||
@@ -3,7 +3,7 @@ KEDA ScaledObjects for various services
|
||||
*/}}
|
||||
|
||||
{{/* Telemetry KEDA ScaledObject */}}
|
||||
{{- if and .Values.keda.enabled .Values.telemetry.enabled .Values.telemetry.keda.enabled (not .Values.telemetry.disableAutoscaler) }}
|
||||
{{- if and .Values.keda.enabled .Values.telemetry.enabled .Values.telemetry.keda.enabled (not .Values.telemetry.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
|
||||
{{- $metricsConfig := dict "enabled" .Values.telemetry.keda.enabled "minReplicas" .Values.telemetry.keda.minReplicas "maxReplicas" .Values.telemetry.keda.maxReplicas "pollingInterval" .Values.telemetry.keda.pollingInterval "cooldownPeriod" .Values.telemetry.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_telemetry_queue_size" "threshold" .Values.telemetry.keda.queueSizeThreshold "port" .Values.telemetry.ports.http)) }}
|
||||
{{- $telemetryKedaArgs := dict "ServiceName" "telemetry" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.telemetry.disableAutoscaler }}
|
||||
{{- include "oneuptime.kedaScaledObject" $telemetryKedaArgs }}
|
||||
@@ -12,7 +12,7 @@ KEDA ScaledObjects for various services
|
||||
{{/* Probe KEDA ScaledObjects - one for each probe configuration */}}
|
||||
{{- range $key, $val := $.Values.probes }}
|
||||
{{- $probeEnabled := or (not (hasKey $val "enabled")) $val.enabled }}
|
||||
{{- if and $.Values.keda.enabled $probeEnabled (and $val.keda $val.keda.enabled) (not $val.disableAutoscaler) }}
|
||||
{{- if and $.Values.keda.enabled $probeEnabled (and $val.keda $val.keda.enabled) (not $val.disableAutoscaler) (not $.Values.deployment.disableDeployments) }}
|
||||
{{- $serviceName := printf "probe-%s" $key }}
|
||||
{{- $probePort := 3874 }}
|
||||
{{- if and $val.ports $val.ports.http }}
|
||||
@@ -25,14 +25,14 @@ KEDA ScaledObjects for various services
|
||||
{{- end }}
|
||||
|
||||
{{/* Worker KEDA ScaledObject */}}
|
||||
{{- if and .Values.keda.enabled .Values.worker.enabled .Values.worker.keda.enabled (not .Values.worker.disableAutoscaler) }}
|
||||
{{- if and .Values.keda.enabled .Values.worker.enabled .Values.worker.keda.enabled (not .Values.worker.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
|
||||
{{- $metricsConfig := dict "enabled" .Values.worker.keda.enabled "minReplicas" .Values.worker.keda.minReplicas "maxReplicas" .Values.worker.keda.maxReplicas "pollingInterval" .Values.worker.keda.pollingInterval "cooldownPeriod" .Values.worker.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_worker_queue_size" "threshold" .Values.worker.keda.queueSizeThreshold "port" .Values.worker.ports.http)) }}
|
||||
{{- $workerKedaArgs := dict "ServiceName" "worker" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.worker.disableAutoscaler }}
|
||||
{{- include "oneuptime.kedaScaledObject" $workerKedaArgs }}
|
||||
{{- end }}
|
||||
|
||||
{{/* AI Agent KEDA ScaledObject */}}
|
||||
{{- if and .Values.keda.enabled .Values.aiAgent.enabled .Values.aiAgent.keda.enabled (not .Values.aiAgent.disableAutoscaler) }}
|
||||
{{- if and .Values.keda.enabled .Values.aiAgent.enabled .Values.aiAgent.keda.enabled (not .Values.aiAgent.disableAutoscaler) (not .Values.deployment.disableDeployments) }}
|
||||
{{- $metricsConfig := dict "enabled" .Values.aiAgent.keda.enabled "minReplicas" .Values.aiAgent.keda.minReplicas "maxReplicas" .Values.aiAgent.keda.maxReplicas "pollingInterval" .Values.aiAgent.keda.pollingInterval "cooldownPeriod" .Values.aiAgent.keda.cooldownPeriod "triggers" (list (dict "query" "oneuptime_ai_agent_queue_size" "threshold" .Values.aiAgent.keda.queueSizeThreshold "port" .Values.aiAgent.ports.http)) }}
|
||||
{{- $aiAgentKedaArgs := dict "ServiceName" "ai-agent" "Release" .Release "Values" .Values "MetricsConfig" $metricsConfig "DisableAutoscaler" .Values.aiAgent.disableAutoscaler }}
|
||||
{{- include "oneuptime.kedaScaledObject" $aiAgentKedaArgs }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.nginx.enabled }}
|
||||
{{- if and $.Values.nginx.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
|
||||
# OneUptime nginx Deployment
|
||||
apiVersion: apps/v1
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if not $.Values.deployment.disableDeployments }}
|
||||
{{- range $key, $val := $.Values.probes }}
|
||||
{{- if or (not (hasKey $val "enabled")) $val.enabled }}
|
||||
apiVersion: apps/v1
|
||||
@@ -166,3 +167,4 @@ spec:
|
||||
---
|
||||
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.telemetry.enabled }}
|
||||
{{- if and $.Values.telemetry.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime telemetry Deployment
|
||||
|
||||
apiVersion: apps/v1
|
||||
@@ -137,7 +137,7 @@ spec:
|
||||
|
||||
---
|
||||
|
||||
{{- if $.Values.telemetry.enabled }}
|
||||
{{- if and $.Values.telemetry.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime telemetry Service
|
||||
{{- $telemetryPorts := dict "http" $.Values.telemetry.ports.http "grpc" $.Values.telemetry.ports.grpc -}}
|
||||
{{- $telemetryServiceArgs := dict "ServiceName" "telemetry" "Ports" $telemetryPorts "Release" $.Release "Values" $.Values -}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.testServer.enabled }}
|
||||
{{- if and $.Values.testServer.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
|
||||
# OneUptime test-server Deployment
|
||||
{{- $testServerPorts := $.Values.testServer.ports -}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if $.Values.worker.enabled }}
|
||||
{{- if and $.Values.worker.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime worker Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -128,7 +128,7 @@ spec:
|
||||
|
||||
---
|
||||
|
||||
{{- if $.Values.worker.enabled }}
|
||||
{{- if and $.Values.worker.enabled (not $.Values.deployment.disableDeployments) }}
|
||||
# OneUptime worker Service
|
||||
{{- $workerPorts := $.Values.worker.ports -}}
|
||||
{{- $workerServiceArgs := dict "ServiceName" "worker" "Ports" $workerPorts "Release" $.Release "Values" $.Values -}}
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
"deployment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disableDeployments": {
|
||||
"type": "boolean",
|
||||
"description": "When set to true, no OneUptime deployments are provisioned. Only databases (Redis, ClickHouse, Postgres) will be running.",
|
||||
"default": false
|
||||
},
|
||||
"includeTimestampLabel": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -48,6 +48,8 @@ externalSecrets:
|
||||
passwordKey:
|
||||
|
||||
deployment:
|
||||
# When set to true, no OneUptime deployments are provisioned. Only databases (Redis, ClickHouse, Postgres) will be running.
|
||||
disableDeployments: false
|
||||
# Default replica count for all deployments
|
||||
replicaCount: 1
|
||||
# Update strategy type for all deployments
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { useTheme } from "../theme";
|
||||
import GradientButton from "./GradientButton";
|
||||
|
||||
type EmptyIcon = "incidents" | "alerts" | "episodes" | "notes" | "monitors" | "default";
|
||||
type EmptyIcon =
|
||||
| "incidents"
|
||||
| "alerts"
|
||||
| "episodes"
|
||||
| "notes"
|
||||
| "monitors"
|
||||
| "default";
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
|
||||
@@ -17,11 +17,14 @@ function toDisplayString(val: unknown): string {
|
||||
}
|
||||
if (typeof val === "object") {
|
||||
// Handle OneUptime typed objects like URL { _type, value }
|
||||
const obj = val as Record<string, unknown>;
|
||||
const obj: Record<string, unknown> = val as Record<string, unknown>;
|
||||
if (typeof obj.value === "string") {
|
||||
return obj.value;
|
||||
}
|
||||
if (typeof obj.toString === "function" && obj.toString !== Object.prototype.toString) {
|
||||
if (
|
||||
typeof obj.toString === "function" &&
|
||||
obj.toString !== Object.prototype.toString
|
||||
) {
|
||||
return obj.toString();
|
||||
}
|
||||
try {
|
||||
@@ -671,7 +674,7 @@ export default function MonitorSummaryView({
|
||||
return getProbeResponse(probe);
|
||||
}, [probeItems, selectedProbeIndex]);
|
||||
|
||||
const renderContent = (): React.JSX.Element => {
|
||||
const renderContent: () => React.JSX.Element = (): React.JSX.Element => {
|
||||
if (!latestResponse) {
|
||||
return (
|
||||
<View style={{ padding: 20, alignItems: "center" }}>
|
||||
|
||||
@@ -142,13 +142,12 @@ export function useAllProjectCounts(): UseAllProjectCountsResult {
|
||||
0,
|
||||
);
|
||||
|
||||
const inoperationalMonitorCount: number =
|
||||
inoperationalMonitorQueries.reduce(
|
||||
(sum: number, q: UseQueryResult<ListResponse<MonitorItem>, Error>) => {
|
||||
return sum + (q.data?.count ?? 0);
|
||||
},
|
||||
0,
|
||||
);
|
||||
const inoperationalMonitorCount: number = inoperationalMonitorQueries.reduce(
|
||||
(sum: number, q: UseQueryResult<ListResponse<MonitorItem>, Error>) => {
|
||||
return sum + (q.data?.count ?? 0);
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
const isLoading: boolean =
|
||||
incidentQuery.isPending ||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user