From c69278ee7e40696b48377803fbcd54256ed76a5a Mon Sep 17 00:00:00 2001 From: Ian McKenzie Date: Wed, 9 Jul 2025 15:41:32 -0700 Subject: [PATCH 1/2] add stdout tab to frontend --- .../components/RunView.nav.test.tsx | 22 ++- .../components/run-page/RunPage.tsx | 9 + .../components/run-page/RunViewModeSwitch.tsx | 6 + .../components/run-page/RunViewStdoutTab.tsx | 163 ++++++++++++++++++ .../run-page/useRunViewActiveTab.tsx | 3 + .../js/src/experiment-tracking/constants.ts | 1 + stdout_tab_guide.md | 89 ++++++++++ 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx create mode 100644 stdout_tab_guide.md diff --git a/mlflow/server/js/src/experiment-tracking/components/RunView.nav.test.tsx b/mlflow/server/js/src/experiment-tracking/components/RunView.nav.test.tsx index a8305bb990328..9f01da7d2460c 100644 --- a/mlflow/server/js/src/experiment-tracking/components/RunView.nav.test.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/RunView.nav.test.tsx @@ -75,6 +75,7 @@ describe('RunView navigation integration test', () => { expect(screen.queryByText('model metric charts')).not.toBeInTheDocument(); expect(screen.queryByText('system metric charts')).not.toBeInTheDocument(); expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument(); + expect(screen.queryByText('stdout tab')).not.toBeInTheDocument(); }); await userEvent.click(screen.getByRole('tab', { name: 'Model metrics' })); @@ -83,6 +84,7 @@ describe('RunView navigation integration test', () => { expect(screen.queryByText('model metric charts')).toBeInTheDocument(); expect(screen.queryByText('system metric charts')).not.toBeInTheDocument(); expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument(); + expect(screen.queryByText('stdout tab')).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('tab', { name: 'System metrics' })); @@ -90,16 +92,34 @@ describe('RunView navigation integration test', () => { expect(screen.queryByText('model metric charts')).not.toBeInTheDocument(); expect(screen.queryByText('system metric charts')).toBeInTheDocument(); expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument(); + expect(screen.queryByText('stdout tab')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('tab', { name: 'Stdout' })); + + expect(screen.queryByText('overview tab')).not.toBeInTheDocument(); + expect(screen.queryByText('model metric charts')).not.toBeInTheDocument(); + expect(screen.queryByText('system metric charts')).not.toBeInTheDocument(); + expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument(); + expect(screen.queryByText('stdout tab')).toBeInTheDocument(); await userEvent.click(screen.getByRole('tab', { name: 'Artifacts' })); expect(screen.queryByText('overview tab')).not.toBeInTheDocument(); expect(screen.queryByText('model metrics')).not.toBeInTheDocument(); expect(screen.queryByText('system metrics')).not.toBeInTheDocument(); + expect(screen.queryByText('stdout tab')).not.toBeInTheDocument(); expect(screen.queryByText('artifacts tab')).toBeInTheDocument(); }); - test('should display artirfact tab if using a targeted artifact URL', async () => { + test('should display stdout tab if using a targeted stdout URL', async () => { + renderComponent('/experiments/123456789/runs/experiment123456789_run1/stdout'); + + await waitFor(() => { + expect(screen.queryByText('stdout tab')).toBeInTheDocument(); + }); + }); + + test('should display artifact tab if using a targeted artifact URL', async () => { renderComponent('/experiments/123456789/runs/experiment123456789_run1/artifacts/model/conda.yaml'); await waitFor(() => { diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunPage.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunPage.tsx index 762cf0a9a2038..16b1ce8b7b4ae 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunPage.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunPage.tsx @@ -11,6 +11,7 @@ import { RenameRunModal } from '../modals/RenameRunModal'; import { RunViewArtifactTab } from './RunViewArtifactTab'; import { RunViewHeader } from './RunViewHeader'; import { RunViewOverview } from './RunViewOverview'; +import { RunViewStdoutTab } from './RunViewStdoutTab'; import { useRunDetailsPageData } from './hooks/useRunDetailsPageData'; import { useRunViewActiveTab } from './useRunViewActiveTab'; import { ReduxState } from '../../../redux-types'; @@ -140,6 +141,14 @@ export const RunPage = () => { artifactUri={runInfo.artifactUri ?? undefined} /> ); + case RunPageTabName.STDOUT: + return ( + + ); case RunPageTabName.TRACES: if (shouldEnableRunDetailsPageTracesTab()) { return ; diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx index c01610b89e043..26f71d9b64c69 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx @@ -75,6 +75,12 @@ export const RunViewModeSwitch = () => { key={RunPageTabName.SYSTEM_METRIC_CHARTS} /> {getLegacyTracesTabLink()} + + } + key={RunPageTabName.STDOUT} + /> diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx new file mode 100644 index 0000000000000..abff1b6d15b56 --- /dev/null +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from 'react'; +import { useDesignSystemTheme, Typography, Empty, DangerIcon } from '@databricks/design-system'; +import { FormattedMessage } from 'react-intl'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { coy as style, atomDark as darkStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { ArtifactViewSkeleton } from '../artifact-view-components/ArtifactViewSkeleton'; +import { fetchArtifactUnified } from '../artifact-view-components/utils/fetchArtifactUnified'; +import { getArtifactContent } from '../../../common/utils/ArtifactUtils'; +import type { KeyValueEntity } from '../../types'; + +/** + * A run page tab containing the stdout log viewer + */ +export const RunViewStdoutTab = ({ + runUuid, + experimentId, + runTags, +}: { + runUuid: string; + experimentId: string; + runTags: Record; +}) => { + const { theme } = useDesignSystemTheme(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [stdoutContent, setStdoutContent] = useState(''); + + useEffect(() => { + const fetchStdoutContent = async () => { + setLoading(true); + setError(null); + + try { + const content = await fetchArtifactUnified( + { + runUuid, + path: 'stdout.log', + experimentId, + isLoggedModelsMode: false, + }, + getArtifactContent + ); + + setStdoutContent(content as string); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + fetchStdoutContent(); + }, [runUuid, experimentId]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ } + title={ + + } + description={ + + } + /> +
+ ); + } + + if (!stdoutContent.trim()) { + return ( +
+ + } + description={ + + } + /> +
+ ); + } + + const syntaxStyle = theme.isDarkMode ? darkStyle : style; + + return ( +
+
+ + + + + + +
+ +
+ + {stdoutContent} + +
+
+ ); +}; \ No newline at end of file diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/useRunViewActiveTab.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/useRunViewActiveTab.tsx index 77bb0231fcd50..3c42f00cb7adc 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/useRunViewActiveTab.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/useRunViewActiveTab.tsx @@ -18,6 +18,9 @@ export const useRunViewActiveTab = (): RunPageTabName => { if (shouldEnableRunDetailsPageTracesTab() && tabParam === 'traces') { return RunPageTabName.TRACES; } + if (tabParam === 'stdout') { + return RunPageTabName.STDOUT; + } if (tabParam?.match(/^(artifactPath|artifacts)/)) { return RunPageTabName.ARTIFACTS; } diff --git a/mlflow/server/js/src/experiment-tracking/constants.ts b/mlflow/server/js/src/experiment-tracking/constants.ts index c67c5ac77e75c..c88e486f972cc 100644 --- a/mlflow/server/js/src/experiment-tracking/constants.ts +++ b/mlflow/server/js/src/experiment-tracking/constants.ts @@ -101,6 +101,7 @@ export enum RunPageTabName { MODEL_METRIC_CHARTS = 'model-metrics', SYSTEM_METRIC_CHARTS = 'system-metrics', ARTIFACTS = 'artifacts', + STDOUT = 'stdout', EVALUATIONS = 'evaluations', } diff --git a/stdout_tab_guide.md b/stdout_tab_guide.md new file mode 100644 index 0000000000000..bdf0eebdf9352 --- /dev/null +++ b/stdout_tab_guide.md @@ -0,0 +1,89 @@ +# MLflow Stdout Tab Guide + +The stdout tab functionality is already implemented in MLflow! This guide shows you how to use it. + +## How It Works + +MLflow captures stdout output during run execution and stores it as an artifact called `stdout.log`. The frontend displays this content in a dedicated "Stdout" tab. + +## Backend Usage + +### Basic Usage + +```python +import mlflow + +with mlflow.start_run(log_stdout=True): + print("This will appear in the stdout tab!") + # Your code here +``` + +### With Custom Interval + +```python +import mlflow + +# Log stdout every 3 seconds instead of default 5 +with mlflow.start_run(log_stdout=True, log_stdout_interval=3): + print("This will appear in the stdout tab!") + # Your code here +``` + +## Frontend Features + +The stdout tab includes: + +- **Syntax highlighting** for better readability +- **Line numbers** for easy navigation +- **Dark/light theme** support +- **Auto-scrolling** and text wrapping +- **Error handling** for missing or empty stdout logs + +## Tab Location + +The stdout tab appears in the run details page alongside: + +- Overview +- Model metrics +- System metrics +- **Stdout** ← Here! +- Artifacts + +## Testing + +Run the test script to verify everything works: + +```bash +python test_stdout_tab.py +``` + +Then: + +1. Open MLflow UI (usually http://localhost:5000) +2. Navigate to the test run +3. Click the "Stdout" tab +4. You should see all the captured output! + +## Implementation Details + +### Backend Files + +- `mlflow/utils/stdout_logging.py` - Core stdout capture logic +- `mlflow/tracking/fluent.py` - Integration with start_run() + +### Frontend Files + +- `mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx` - Tab component +- `mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx` - Tab switcher +- `mlflow/server/js/src/experiment-tracking/constants.ts` - Tab definitions + +## Similar to Wandb + +This provides similar functionality to Wandb's stdout logging, where you can: + +- View real-time stdout output +- Navigate through logs easily +- Keep logs organized per run +- Access logs directly from the UI + +The stdout tab is ready to use - no additional setup required! From fa804d3b29b9c1ebdccf8ce106dbe88cc349e9a9 Mon Sep 17 00:00:00 2001 From: Ian McKenzie Date: Wed, 9 Jul 2025 15:47:28 -0700 Subject: [PATCH 2/2] remove line numbers (they weren't aligned) --- .../components/run-page/RunViewStdoutTab.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx index abff1b6d15b56..f3f54e840eb49 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx @@ -147,13 +147,12 @@ export const RunViewStdoutTab = ({ margin: 0, padding: theme.spacing.md, backgroundColor: theme.colors.backgroundSecondary, - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', + whiteSpace: 'pre', height: '100%', overflow: 'auto', }} - showLineNumbers - wrapLongLines + showLineNumbers={false} + wrapLongLines={false} > {stdoutContent}