Skip to content

Commit

Permalink
feat(provider): added auto attributes, fixed issue on activity-logs
Browse files Browse the repository at this point in the history
  • Loading branch information
jigar-arc10 committed Feb 20, 2025
1 parent d0da229 commit 72dddf0
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 99 deletions.
18 changes: 16 additions & 2 deletions apps/provider-console/sentry.client.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN,
integrations: [Sentry.replayIntegration()],
integrations: [
Sentry.replayIntegration(),
new Sentry.BrowserTracing({
// Set sampling rate for performance monitoring
tracePropagationTargets: ["localhost", /^https:\/\/provider-console.akash.network/]
})
],
// Session replay sampling rates
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0
replaysOnErrorSampleRate: 1.0,

// Performance monitoring sampling rate
tracesSampleRate: 0.2,

// Enable automatic instrumentation for analytics
enableAutoSessionTracking: true,
sessionTrackingIntervalMillis: 30000
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,68 @@ const attributeKeys = Object.keys(providerAttributesFormValuesSchema.shape);

const DEFAULT_ATTRIBUTES = ["host", "tier"];

interface GpuConfig {
count: number;
vendor: string;
name: string;
memory_size: string;
interface: string;
}

const createGpuAttributes = (gpuConfigs: GpuConfig[] | undefined) => {
// Return empty array if gpuConfigs is undefined, empty, or has no valid GPUs
if (!gpuConfigs || gpuConfigs.length === 0) return [];

// Filter out configurations with count=0 or null values
const validGpuConfigs = gpuConfigs.filter(
gpu => gpu.count !== 0 && gpu.vendor !== null && gpu.name !== null && gpu.memory_size !== null && gpu.interface !== null
);

if (validGpuConfigs.length === 0) return [];

// Get unique GPU configurations based on vendor, model, memory, and interface
const uniqueConfigs = validGpuConfigs.reduce(
(acc, gpu) => {
const key = `${gpu.vendor}-${gpu.name}-${gpu.memory_size}-${gpu.interface}`;
if (!acc[key]) {
acc[key] = gpu;
}
return acc;
},
{} as Record<string, GpuConfig>
);

return Object.values(uniqueConfigs).flatMap(gpu => {
const vendor = gpu.vendor.toLowerCase();
const model = gpu.name.toLowerCase();
const memory = gpu.memory_size;
const iface = gpu.interface.toLowerCase();

return [
{
key: "unknown-attributes",
value: "true",
customKey: `capabilities/gpu/vendor/${vendor}/model/${model}`
},
{
key: "unknown-attributes",
value: "true",
customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/ram/${memory}`
},
{
key: "unknown-attributes",
value: "true",
customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/ram/${memory}/interface/${iface}`
},
{
key: "unknown-attributes",
value: "true",
customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/interface/${iface}`
}
];
});
};

interface ProviderAttributesProps {
existingAttributes?: ProviderAttribute[];
editMode?: boolean;
Expand All @@ -62,6 +124,9 @@ export const ProviderAttributes: React.FunctionComponent<ProviderAttributesProps
const [providerProcess, setProviderProcess] = useAtom(providerProcessStore.providerProcessAtom);
const organizationName = providerProcess.config?.organization;

// Collect GPU configurations from all machines
const gpuConfigs = providerProcess.machines?.map(machine => machine.systemInfo?.gpu).filter((gpu): gpu is GpuConfig => !!gpu);

const form = useForm<ProviderFormValues>({
resolver: zodResolver(providerFormSchema),
defaultValues: {
Expand All @@ -74,7 +139,8 @@ export const ProviderAttributes: React.FunctionComponent<ProviderAttributesProps
: [
{ key: "host", value: "akash", customKey: "" },
{ key: "tier", value: "community", customKey: "" },
{ key: "organization", value: organizationName || "", customKey: "" }
{ key: "organization", value: organizationName || "", customKey: "" },
...(gpuConfigs?.length ? createGpuAttributes(gpuConfigs) : [])
]
}
});
Expand Down
133 changes: 94 additions & 39 deletions apps/provider-console/src/components/shared/ActivityLogDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,31 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti
const [openAccordions, setOpenAccordions] = useState<boolean[]>([]);
const [taskLogs, setTaskLogs] = useState<TaskLogs>({});
const [loadingLogs, setLoadingLogs] = useState<{ [taskId: string]: boolean }>({});
const [elapsedTimes, setElapsedTimes] = useState<{ [taskId: string]: string }>({});
const logStreams = useRef<{ [taskId: string]: EventSourcePolyfill | null }>({});
const { data: actionDetails, isLoading } = useProviderActionStatus(actionId);

useEffect(() => {
if (actionDetails) {
if (actionDetails && actionDetails.tasks) {
const initialAccordions = actionDetails.tasks?.map(task => task.status === "in_progress");
setOpenAccordions(initialAccordions ?? []);
}
}, [actionDetails]);

useEffect(() => {
if (actionDetails?.tasks) {
Object.values(logStreams.current).forEach(stream => stream?.close());
if (actionDetails?.tasks?.length && actionDetails.tasks.length > 0) {
// Safely close existing streams
if (logStreams.current) {
Object.values(logStreams.current).forEach(stream => {
if (stream && typeof stream.close === "function") {
try {
stream.close();
} catch (error) {
console.warn("Error closing stream:", error);
}
}
});
}
logStreams.current = {};

actionDetails.tasks.forEach((task, index) => {
Expand All @@ -41,19 +53,54 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti
setupLogStream(task.id);
} else {
if (logStreams.current[task.id]) {
logStreams.current[task.id]?.close();
try {
logStreams.current[task.id]?.close();
} catch (error) {
console.warn("Error closing stream:", error);
}
logStreams.current[task.id] = null;
}
}
});
}

return () => {
Object.values(logStreams.current).forEach(stream => stream?.close());
// Cleanup function
if (logStreams.current) {
Object.values(logStreams.current).forEach(stream => {
if (stream && typeof stream.close === "function") {
try {
stream.close();
} catch (error) {
console.warn("Error closing stream:", error);
}
}
});
}
logStreams.current = {};
};
}, [actionDetails?.tasks]);

useEffect(() => {
if (!actionDetails?.tasks) return;

const interval = setInterval(() => {
const newElapsedTimes: { [taskId: string]: string } = {};

actionDetails?.tasks?.forEach(task => {
if (task.status === "in_progress" && task.start_time) {
newElapsedTimes[task.id] = formatTimeLapse(task.start_time, null);
} else if (task.start_time && task.end_time) {
newElapsedTimes[task.id] = formatTimeLapse(task.start_time, task.end_time);
}
});

setElapsedTimes(newElapsedTimes);
}, 1000);

return () => clearInterval(interval);
}, [actionDetails?.tasks]);

const fetchTaskLogs = async (taskId: string) => {
setLoadingLogs(prev => ({ ...prev, [taskId]: true }));
try {
Expand All @@ -79,6 +126,8 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti
eventSource.onmessage = event => {
try {
const logData = JSON.parse(event.data);
if (!logData || !logData.message) return; // Skip invalid messages

const formattedMessage = `${logData.message}`;

setTaskLogs(prev => ({
Expand Down Expand Up @@ -125,38 +174,52 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti
);
}

if (!logs) {
if (!logs || typeof logs !== "string") {
return (
<div className="text-muted-foreground mt-4 flex items-center justify-center" style={{ height: 200 }}>
No logs recorded for this task or this task is more than 7 days old.
</div>
);
}

return (
<div className="mt-4" style={{ height: 200 }}>
<ScrollFollow
startFollowing={true}
render={({ follow, onScroll }) => (
<LazyLog
text={logs}
follow={follow}
onScroll={onScroll}
highlight={[]}
extraLines={1}
ansi
caseInsensitive
selectableLines
enableLineNumbers={false}
containerStyle={{
maxHeight: "200px",
borderRadius: "0.375rem"
}}
/>
)}
/>
</div>
);
try {
const sanitizedLogs = logs.trim();

return (
<div className="mt-4" style={{ height: 200 }}>
<ScrollFollow
startFollowing={true}
render={({ onScroll }) => (
<LazyLog
text={sanitizedLogs}
follow={true}
onScroll={onScroll}
highlight={[]}
extraLines={1}
ansi
caseInsensitive
selectableLines
enableLineNumbers={false}
containerStyle={{
maxHeight: "200px",
borderRadius: "0.375rem",
backgroundColor: "var(--log-background, #1e1e1e)",
color: "var(--log-text, #ffffff)"
}}
key={`${taskId}-${sanitizedLogs.length}`}
scrollToLine={sanitizedLogs.split("\n").length}
/>
)}
/>
</div>
);
} catch (error) {
return (
<div className="mt-4 rounded-md bg-gray-100 p-4 dark:bg-gray-800" style={{ height: 200, overflowY: "auto" }}>
<pre className="whitespace-pre-wrap break-words text-sm">{logs}</pre>
</div>
);
}
};

if (actionId === null) {
Expand Down Expand Up @@ -205,15 +268,7 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti
<span>{task.description}</span>
</div>
<div className="flex items-center">
{task.start_time && (
<p className="text-muted-foreground mr-2 text-xs">
{task.status === "in_progress"
? formatTimeLapse(task.start_time, null)
: task.end_time
? formatTimeLapse(task.start_time, task.end_time)
: ""}
</p>
)}
{task.start_time && <p className="text-muted-foreground mr-2 text-xs">{elapsedTimes[task.id] || ""}</p>}
{task.status === "completed" && <Check className="h-4 w-4 text-green-500" />}
{task.status === "in_progress" && <Spinner className="text-blue-500" size="small" />}
{task.status === "not_started" && <div className="h-5 w-5 rounded-full border-2"></div>}
Expand Down
19 changes: 7 additions & 12 deletions apps/provider-console/src/components/shared/ActivityLogList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useCallback } from "react";
import { Separator } from "@akashnetwork/ui/components";
import { CheckCircle, Play, XmarkCircle } from "iconoir-react";
import { Separator, Spinner } from "@akashnetwork/ui/components";
import { CheckCircle, XmarkCircle } from "iconoir-react";
import { useRouter } from "next/router";

import { formatTimeLapse } from "@src/utils/dateUtils";

interface ProviderAction {
id: string;
name: string;
Expand All @@ -24,7 +26,7 @@ const StatusIcon: React.FC<StatusIconProps> = ({ status }) => {
case "completed":
return <CheckCircle className="text-green-500" />;
case "in_progress":
return <Play className="text-blue-500" />;
return <Spinner size="small" />;
case "failed":
return <XmarkCircle className="text-red-500" />;
default:
Expand All @@ -49,13 +51,6 @@ export const ActivityLogList: React.FC<ActivityLogsListProps> = ({ actions }) =>
});
}, []);

const calculateTimeLapse = (start: string, end?: string) => {
const startTime = new Date(start).getTime();
const endTime = end ? new Date(end).getTime() : Date.now();
const timeLapse = endTime - startTime;
return `${Math.floor(timeLapse / 1000)} seconds`;
};

const handleRowClick = (actionId: string) => {
router.push(`/activity-logs/${actionId}`);
};
Expand All @@ -65,7 +60,7 @@ export const ActivityLogList: React.FC<ActivityLogsListProps> = ({ actions }) =>
<div className="mb-4 grid grid-cols-12 items-center gap-4 px-4 text-sm font-medium text-gray-500">
<div className="col-span-4">Action</div>
<div className="col-span-2">Duration</div>
<div className="col-span-4">Timestamp</div>
<div className="col-span-4">Start Time</div>
<div className="col-span-2 text-right">Status</div>
</div>
<Separator />
Expand All @@ -78,7 +73,7 @@ export const ActivityLogList: React.FC<ActivityLogsListProps> = ({ actions }) =>
<p className="text-sm font-medium">{action.name}</p>
</div>
<div className="col-span-2">
<p className="text-muted-foreground text-sm">{calculateTimeLapse(action.start_time, action.end_time)}</p>
<p className="text-muted-foreground text-sm">{formatTimeLapse(action.start_time, action.end_time || null)}</p>
</div>
<div className="col-span-4">
<p className="text-muted-foreground text-sm">{formatDate(action.start_time)}</p>
Expand Down
Loading

0 comments on commit 72dddf0

Please sign in to comment.