oneuptime/Common/UI/Components/LogsViewer/LogsViewer.tsx
Nawaz Dhandala e7089e9e85
Refactor TelemetryService to Service across the application
- Replaced all instances of TelemetryService with Service in components, pages, and utilities.
- Updated related imports and state management to reflect the new Service model.
- Removed the TelemetryServices view and associated routes, as it is no longer needed.
- Adjusted breadcrumb and route mappings to remove references to Telemetry Services.
- Ensured that all relevant functionality, such as logs and metrics, now utilize the Service model.
2026-01-09 15:49:52 +00:00

494 lines
14 KiB
TypeScript

import React, {
FunctionComponent,
ReactElement,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import Query from "../../../Types/BaseDatabase/Query";
import { PromiseVoidFunction } from "../../../Types/FunctionTypes";
import Log from "../../../Models/AnalyticsModels/Log";
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
import Route from "../../../Types/API/Route";
import URL from "../../../Types/API/URL";
import HTTPResponse from "../../../Types/API/HTTPResponse";
import { JSONObject } from "../../../Types/JSON";
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
import API from "../../Utils/API/API";
import { APP_API_URL } from "../../Config";
import PageLoader from "../Loader/PageLoader";
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import Service from "../../../Models/DatabaseModels/Service";
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
import ListResult from "../../../Types/BaseDatabase/ListResult";
import Dictionary from "../../../Types/Dictionary";
import LogsFilterCard from "./components/LogsFilterCard";
import LogsViewerToolbar, {
LogsViewerToolbarProps,
} from "./components/LogsViewerToolbar";
import LogsTable, {
LogsTableSortField,
resolveLogIdentifier,
} from "./components/LogsTable";
import LogsPagination from "./components/LogsPagination";
import LogDetailsPanel from "./components/LogDetailsPanel";
import { LiveLogsOptions } from "./types";
export interface ComponentProps {
logs: Array<Log>;
onFilterChanged: (filterOptions: Query<Log>) => void;
filterData: Query<Log>;
isLoading: boolean;
showFilters?: boolean | undefined;
noLogsMessage?: string | undefined;
getTraceRoute?: (traceId: string, log: Log) => Route | URL | undefined;
getSpanRoute?: (spanId: string, log: Log) => Route | URL | undefined;
totalCount?: number | undefined;
page?: number | undefined;
pageSize?: number | undefined;
onPageChange?: (page: number) => void;
onPageSizeChange?: (size: number) => void;
sortField?: LogsTableSortField | undefined;
sortOrder?: SortOrder | undefined;
onSortChange?: (field: LogsTableSortField, order: SortOrder) => void;
liveOptions?: LiveLogsOptions | undefined;
}
export type LogsSortField = LogsTableSortField;
export type { LiveLogsOptions } from "./types";
const DEFAULT_PAGE_SIZE: number = 100;
const PAGE_SIZE_OPTIONS: Array<number> = [100, 250, 500, 1000];
const severityWeight: Record<string, number> = {
fatal: 6,
error: 5,
warn: 4,
warning: 4,
info: 3,
notice: 3,
debug: 2,
trace: 1,
};
const getSeverityWeight: (severity: string | undefined) => number = (
severity: string | undefined,
): number => {
if (!severity) {
return 0;
}
const normalized: string = severity.toString().toLowerCase();
return severityWeight[normalized] || 0;
};
const LogsViewer: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [filterData, setFilterData] = useState<Query<Log>>(props.filterData);
const [logAttributes, setLogAttributes] = useState<Array<string>>([]);
const [attributesLoaded, setAttributesLoaded] = useState<boolean>(false);
const [attributesLoading, setAttributesLoading] = useState<boolean>(false);
const [attributesError, setAttributesError] = useState<string>("");
const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] =
useState<boolean>(false);
const [isPageLoading, setIsPageLoading] = useState<boolean>(true);
const [pageError, setPageError] = useState<string>("");
const [serviceMap, setServiceMap] = useState<Dictionary<Service>>(
{},
);
const [selectedLogId, setSelectedLogId] = useState<string | null>(null);
const [internalPage, setInternalPage] = useState<number>(1);
const [internalPageSize, setInternalPageSize] =
useState<number>(DEFAULT_PAGE_SIZE);
const [localSortField, setLocalSortField] =
useState<LogsTableSortField>("time");
const [localSortOrder, setLocalSortOrder] = useState<SortOrder>(
SortOrder.Descending,
);
useEffect(() => {
setFilterData(props.filterData);
}, [props.filterData]);
useEffect(() => {
if (props.sortField) {
setLocalSortField(props.sortField);
}
}, [props.sortField]);
useEffect(() => {
if (props.sortOrder) {
setLocalSortOrder(props.sortOrder);
}
}, [props.sortOrder]);
useEffect(() => {
if (props.pageSize) {
setInternalPageSize(props.pageSize);
}
}, [props.pageSize]);
const currentPage: number = props.page ?? internalPage;
const pageSize: number = props.pageSize ?? internalPageSize;
const totalItems: number = props.totalCount ?? props.logs.length;
const totalPages: number = Math.max(
1,
Math.ceil(totalItems / Math.max(pageSize, 1)),
);
const sortField: LogsTableSortField = props.sortField ?? localSortField;
const sortOrder: SortOrder = props.sortOrder ?? localSortOrder;
const shouldClientSort: boolean = !props.onSortChange;
const sortedLogs: Array<Log> = useMemo(() => {
if (!shouldClientSort) {
return props.logs;
}
const cloned: Array<Log> = [...props.logs];
cloned.sort((a: Log, b: Log) => {
if (sortField === "time") {
const aTime: number =
Number(a.timeUnixNano) || (a.time ? new Date(a.time).getTime() : 0);
const bTime: number =
Number(b.timeUnixNano) || (b.time ? new Date(b.time).getTime() : 0);
if (aTime === bTime) {
return 0;
}
return sortOrder === SortOrder.Descending
? bTime - aTime
: aTime - bTime;
}
const aSeverity: number = getSeverityWeight(a.severityText?.toString());
const bSeverity: number = getSeverityWeight(b.severityText?.toString());
if (aSeverity === bSeverity) {
return 0;
}
return sortOrder === SortOrder.Descending
? bSeverity - aSeverity
: aSeverity - bSeverity;
});
return cloned;
}, [props.logs, shouldClientSort, sortField, sortOrder]);
const shouldClientPaginate: boolean = props.totalCount === undefined;
const paginatedLogs: Array<Log> = useMemo(() => {
if (!shouldClientPaginate) {
return sortedLogs;
}
if (sortedLogs.length === 0) {
return [];
}
const safePage: number = Math.min(Math.max(currentPage, 1), totalPages);
const startIndex: number = (safePage - 1) * pageSize;
return sortedLogs.slice(startIndex, startIndex + pageSize);
}, [sortedLogs, shouldClientPaginate, currentPage, totalPages, pageSize]);
const displayedLogs: Array<Log> = shouldClientPaginate
? paginatedLogs
: sortedLogs;
useEffect(() => {
if (!shouldClientPaginate || props.page !== undefined) {
return;
}
const safePage: number = Math.min(Math.max(internalPage, 1), totalPages);
if (safePage !== internalPage) {
setInternalPage(safePage);
}
}, [shouldClientPaginate, props.page, internalPage, totalPages]);
useEffect(() => {
if (!selectedLogId) {
return;
}
const stillExists: boolean = displayedLogs.some(
(log: Log, index: number) => {
return resolveLogIdentifier(log, index) === selectedLogId;
},
);
if (!stillExists) {
setSelectedLogId(null);
}
}, [displayedLogs, selectedLogId]);
const loadServices: PromiseVoidFunction =
useCallback(async (): Promise<void> => {
try {
setIsPageLoading(true);
setPageError("");
const telemetryServices: ListResult<Service> =
await ModelAPI.getList({
modelType: Service,
query: {},
select: {
name: true,
serviceColor: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
const services: Dictionary<Service> = {};
telemetryServices.data.forEach((service: Service) => {
if (!service.id) {
return;
}
services[service.id.toString()] = service;
});
setServiceMap(services);
} catch (err) {
setPageError(
`We couldn't load telemetry service metadata. ${API.getFriendlyErrorMessage(err as Error)}`,
);
} finally {
setIsPageLoading(false);
}
}, []);
const loadAttributes: PromiseVoidFunction =
useCallback(async (): Promise<void> => {
try {
setAttributesLoading(true);
setAttributesError("");
const attributeResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/telemetry/logs/get-attributes",
),
data: {},
headers: {
...ModelAPI.getCommonHeaders(),
},
});
if (attributeResponse instanceof HTTPErrorResponse) {
throw attributeResponse;
}
const attributes: Array<string> = (attributeResponse.data[
"attributes"
] || []) as Array<string>;
setLogAttributes(attributes);
setAttributesLoaded(true);
} catch (err) {
setLogAttributes([]);
setAttributesLoaded(false);
setAttributesError(
`We couldn't load log attributes. Filters may be limited. ${API.getFriendlyErrorMessage(err as Error)}`,
);
} finally {
setAttributesLoading(false);
}
}, []);
useEffect(() => {
void loadServices();
}, [loadServices]);
const resetPage: () => void = (): void => {
if (props.onPageChange) {
props.onPageChange(1);
}
if (props.page === undefined) {
setInternalPage(1);
}
};
const handleApplyFilters: () => void = (): void => {
resetPage();
setSelectedLogId(null);
props.onFilterChanged(filterData);
};
const handlePageChange: (page: number) => void = (page: number): void => {
if (props.onPageChange) {
props.onPageChange(page);
}
if (props.page === undefined) {
setInternalPage(page);
}
setSelectedLogId(null);
setSelectedLogId(null);
};
const handlePageSizeChange: (size: number) => void = (size: number): void => {
if (props.onPageSizeChange) {
props.onPageSizeChange(size);
}
if (props.pageSize === undefined) {
setInternalPageSize(size);
}
resetPage();
setSelectedLogId(null);
};
const handleSortChange: (field: LogsTableSortField) => void = (
field: LogsTableSortField,
): void => {
const isSameField: boolean = sortField === field;
const nextOrder: SortOrder = isSameField
? sortOrder === SortOrder.Descending
? SortOrder.Ascending
: SortOrder.Descending
: SortOrder.Descending;
setLocalSortField(field);
setLocalSortOrder(nextOrder);
props.onSortChange?.(field, nextOrder);
resetPage();
setSelectedLogId(null);
};
if (isPageLoading) {
return <PageLoader isVisible={true} />;
}
if (pageError) {
return <ErrorMessage message={pageError} />;
}
const toolbarProps: LogsViewerToolbarProps = {
resultCount: totalItems,
currentPage,
totalPages,
...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
};
return (
<div className="space-y-6">
{props.showFilters && (
<div className="mb-6">
<LogsFilterCard
filterData={filterData}
onFilterChanged={(updated: Query<Log>) => {
setFilterData(updated);
}}
onAdvancedFiltersToggle={(show: boolean) => {
setAreAdvancedFiltersVisible(show);
if (show && !attributesLoaded && !attributesLoading) {
void loadAttributes();
}
}}
isFilterLoading={areAdvancedFiltersVisible && attributesLoading}
filterError={
areAdvancedFiltersVisible && attributesError
? attributesError
: undefined
}
onFilterRefreshClick={
areAdvancedFiltersVisible && attributesError
? () => {
void loadAttributes();
}
: undefined
}
logAttributes={logAttributes}
toolbar={
<LogsViewerToolbar
{...toolbarProps}
showApplyButton={true}
onApplyFilters={handleApplyFilters}
/>
}
/>
</div>
)}
<div className="overflow-hidden rounded-2xl border border-slate-800/70 bg-slate-950/60 shadow-xl">
{!props.showFilters && (
<div className="border-b border-slate-800/70 bg-slate-950/70 px-4 py-3">
<LogsViewerToolbar {...toolbarProps} />
</div>
)}
<LogsTable
logs={displayedLogs}
serviceMap={serviceMap}
isLoading={props.isLoading}
emptyMessage={props.noLogsMessage}
onRowClick={(_log: Log, rowId: string) => {
setSelectedLogId((currentSelected: string | null) => {
if (currentSelected === rowId) {
return null;
}
return rowId;
});
}}
selectedLogId={selectedLogId}
sortField={sortField}
sortOrder={sortOrder}
onSortChange={handleSortChange}
renderExpandedContent={(log: Log) => {
return (
<LogDetailsPanel
log={log}
serviceMap={serviceMap}
onClose={() => {
setSelectedLogId(null);
}}
getTraceRoute={props.getTraceRoute}
getSpanRoute={props.getSpanRoute}
variant="embedded"
/>
);
}}
/>
<LogsPagination
currentPage={currentPage}
totalItems={totalItems}
pageSize={pageSize}
pageSizeOptions={PAGE_SIZE_OPTIONS}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
isDisabled={props.isLoading || totalItems === 0}
/>
</div>
</div>
);
};
export default LogsViewer;