Skip to content

Commit 7d4c065

Browse files
committed
add stdout tab to frontend
1 parent 76e0520 commit 7d4c065

File tree

7 files changed

+292
-1
lines changed

7 files changed

+292
-1
lines changed

mlflow/server/js/src/experiment-tracking/components/RunView.nav.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ describe('RunView navigation integration test', () => {
7575
expect(screen.queryByText('model metric charts')).not.toBeInTheDocument();
7676
expect(screen.queryByText('system metric charts')).not.toBeInTheDocument();
7777
expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument();
78+
expect(screen.queryByText('stdout tab')).not.toBeInTheDocument();
7879
});
7980

8081
await userEvent.click(screen.getByRole('tab', { name: 'Model metrics' }));
@@ -83,23 +84,42 @@ describe('RunView navigation integration test', () => {
8384
expect(screen.queryByText('model metric charts')).toBeInTheDocument();
8485
expect(screen.queryByText('system metric charts')).not.toBeInTheDocument();
8586
expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument();
87+
expect(screen.queryByText('stdout tab')).not.toBeInTheDocument();
8688

8789
await userEvent.click(screen.getByRole('tab', { name: 'System metrics' }));
8890

8991
expect(screen.queryByText('overview tab')).not.toBeInTheDocument();
9092
expect(screen.queryByText('model metric charts')).not.toBeInTheDocument();
9193
expect(screen.queryByText('system metric charts')).toBeInTheDocument();
9294
expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument();
95+
expect(screen.queryByText('stdout tab')).not.toBeInTheDocument();
96+
97+
await userEvent.click(screen.getByRole('tab', { name: 'Stdout' }));
98+
99+
expect(screen.queryByText('overview tab')).not.toBeInTheDocument();
100+
expect(screen.queryByText('model metric charts')).not.toBeInTheDocument();
101+
expect(screen.queryByText('system metric charts')).not.toBeInTheDocument();
102+
expect(screen.queryByText('artifacts tab')).not.toBeInTheDocument();
103+
expect(screen.queryByText('stdout tab')).toBeInTheDocument();
93104

94105
await userEvent.click(screen.getByRole('tab', { name: 'Artifacts' }));
95106

96107
expect(screen.queryByText('overview tab')).not.toBeInTheDocument();
97108
expect(screen.queryByText('model metrics')).not.toBeInTheDocument();
98109
expect(screen.queryByText('system metrics')).not.toBeInTheDocument();
110+
expect(screen.queryByText('stdout tab')).not.toBeInTheDocument();
99111
expect(screen.queryByText('artifacts tab')).toBeInTheDocument();
100112
});
101113

102-
test('should display artirfact tab if using a targeted artifact URL', async () => {
114+
test('should display stdout tab if using a targeted stdout URL', async () => {
115+
renderComponent('/experiments/123456789/runs/experiment123456789_run1/stdout');
116+
117+
await waitFor(() => {
118+
expect(screen.queryByText('stdout tab')).toBeInTheDocument();
119+
});
120+
});
121+
122+
test('should display artifact tab if using a targeted artifact URL', async () => {
103123
renderComponent('/experiments/123456789/runs/experiment123456789_run1/artifacts/model/conda.yaml');
104124

105125
await waitFor(() => {

mlflow/server/js/src/experiment-tracking/components/run-page/RunPage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RenameRunModal } from '../modals/RenameRunModal';
1111
import { RunViewArtifactTab } from './RunViewArtifactTab';
1212
import { RunViewHeader } from './RunViewHeader';
1313
import { RunViewOverview } from './RunViewOverview';
14+
import { RunViewStdoutTab } from './RunViewStdoutTab';
1415
import { useRunDetailsPageData } from './hooks/useRunDetailsPageData';
1516
import { useRunViewActiveTab } from './useRunViewActiveTab';
1617
import { ReduxState } from '../../../redux-types';
@@ -140,6 +141,14 @@ export const RunPage = () => {
140141
artifactUri={runInfo.artifactUri ?? undefined}
141142
/>
142143
);
144+
case RunPageTabName.STDOUT:
145+
return (
146+
<RunViewStdoutTab
147+
runUuid={runUuid}
148+
experimentId={experimentId}
149+
runTags={tags}
150+
/>
151+
);
143152
case RunPageTabName.TRACES:
144153
if (shouldEnableRunDetailsPageTracesTab()) {
145154
return <RunViewTracesTab runUuid={runUuid} runTags={tags} experimentId={experimentId} />;

mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export const RunViewModeSwitch = () => {
7575
key={RunPageTabName.SYSTEM_METRIC_CHARTS}
7676
/>
7777
{getLegacyTracesTabLink()}
78+
<LegacyTabs.TabPane
79+
tab={
80+
<FormattedMessage defaultMessage="Stdout" description="Run details page > tab selector > Stdout tab" />
81+
}
82+
key={RunPageTabName.STDOUT}
83+
/>
7884
<LegacyTabs.TabPane
7985
tab={
8086
<FormattedMessage defaultMessage="Artifacts" description="Run details page > tab selector > artifacts tab" />
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { useDesignSystemTheme, Typography, Empty, DangerIcon } from '@databricks/design-system';
3+
import { FormattedMessage } from 'react-intl';
4+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5+
import { coy as style, atomDark as darkStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism';
6+
import { ArtifactViewSkeleton } from '../artifact-view-components/ArtifactViewSkeleton';
7+
import { fetchArtifactUnified } from '../artifact-view-components/utils/fetchArtifactUnified';
8+
import { getArtifactContent } from '../../../common/utils/ArtifactUtils';
9+
import type { KeyValueEntity } from '../../types';
10+
11+
/**
12+
* A run page tab containing the stdout log viewer
13+
*/
14+
export const RunViewStdoutTab = ({
15+
runUuid,
16+
experimentId,
17+
runTags,
18+
}: {
19+
runUuid: string;
20+
experimentId: string;
21+
runTags: Record<string, KeyValueEntity>;
22+
}) => {
23+
const { theme } = useDesignSystemTheme();
24+
const [loading, setLoading] = useState(true);
25+
const [error, setError] = useState<Error | null>(null);
26+
const [stdoutContent, setStdoutContent] = useState<string>('');
27+
28+
useEffect(() => {
29+
const fetchStdoutContent = async () => {
30+
setLoading(true);
31+
setError(null);
32+
33+
try {
34+
const content = await fetchArtifactUnified(
35+
{
36+
runUuid,
37+
path: 'stdout.log',
38+
experimentId,
39+
isLoggedModelsMode: false,
40+
},
41+
getArtifactContent
42+
);
43+
44+
setStdoutContent(content as string);
45+
} catch (err) {
46+
setError(err as Error);
47+
} finally {
48+
setLoading(false);
49+
}
50+
};
51+
52+
fetchStdoutContent();
53+
}, [runUuid, experimentId]);
54+
55+
if (loading) {
56+
return (
57+
<div css={{ flex: 1, padding: theme.spacing.md }}>
58+
<ArtifactViewSkeleton className="stdout-loading" />
59+
</div>
60+
);
61+
}
62+
63+
if (error) {
64+
return (
65+
<div css={{ flex: 1, padding: theme.spacing.md }}>
66+
<Empty
67+
image={<DangerIcon />}
68+
title={
69+
<FormattedMessage
70+
defaultMessage="No stdout logs found"
71+
description="Run page > stdout tab > no stdout logs title"
72+
/>
73+
}
74+
description={
75+
<FormattedMessage
76+
defaultMessage="This run does not have stdout logging enabled or no stdout.log artifact was found. To enable stdout logging, set log_stdout=True when calling mlflow.start_run()."
77+
description="Run page > stdout tab > no stdout logs description"
78+
/>
79+
}
80+
/>
81+
</div>
82+
);
83+
}
84+
85+
if (!stdoutContent.trim()) {
86+
return (
87+
<div css={{ flex: 1, padding: theme.spacing.md }}>
88+
<Empty
89+
title={
90+
<FormattedMessage
91+
defaultMessage="No stdout output"
92+
description="Run page > stdout tab > empty stdout title"
93+
/>
94+
}
95+
description={
96+
<FormattedMessage
97+
defaultMessage="The stdout.log file exists but contains no content."
98+
description="Run page > stdout tab > empty stdout description"
99+
/>
100+
}
101+
/>
102+
</div>
103+
);
104+
}
105+
106+
const syntaxStyle = theme.isDarkMode ? darkStyle : style;
107+
108+
return (
109+
<div
110+
css={{
111+
flex: 1,
112+
display: 'flex',
113+
flexDirection: 'column',
114+
overflow: 'hidden',
115+
padding: theme.spacing.md,
116+
}}
117+
>
118+
<div css={{ marginBottom: theme.spacing.sm }}>
119+
<Typography.Title level={3}>
120+
<FormattedMessage
121+
defaultMessage="Standard Output"
122+
description="Run page > stdout tab > title"
123+
/>
124+
</Typography.Title>
125+
<Typography.Text color="secondary">
126+
<FormattedMessage
127+
defaultMessage="Real-time stdout logs captured during run execution"
128+
description="Run page > stdout tab > subtitle"
129+
/>
130+
</Typography.Text>
131+
</div>
132+
133+
<div
134+
css={{
135+
flex: 1,
136+
overflow: 'auto',
137+
border: `1px solid ${theme.colors.borderDecorative}`,
138+
borderRadius: theme.borders.borderRadiusMd,
139+
}}
140+
>
141+
<SyntaxHighlighter
142+
language="text"
143+
style={syntaxStyle}
144+
customStyle={{
145+
fontFamily: 'Source Code Pro, Menlo, Monaco, monospace',
146+
fontSize: theme.typography.fontSizeSm,
147+
margin: 0,
148+
padding: theme.spacing.md,
149+
backgroundColor: theme.colors.backgroundSecondary,
150+
whiteSpace: 'pre-wrap',
151+
wordBreak: 'break-word',
152+
height: '100%',
153+
overflow: 'auto',
154+
}}
155+
showLineNumbers
156+
wrapLongLines
157+
>
158+
{stdoutContent}
159+
</SyntaxHighlighter>
160+
</div>
161+
</div>
162+
);
163+
};

mlflow/server/js/src/experiment-tracking/components/run-page/useRunViewActiveTab.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const useRunViewActiveTab = (): RunPageTabName => {
1818
if (shouldEnableRunDetailsPageTracesTab() && tabParam === 'traces') {
1919
return RunPageTabName.TRACES;
2020
}
21+
if (tabParam === 'stdout') {
22+
return RunPageTabName.STDOUT;
23+
}
2124
if (tabParam?.match(/^(artifactPath|artifacts)/)) {
2225
return RunPageTabName.ARTIFACTS;
2326
}

mlflow/server/js/src/experiment-tracking/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export enum RunPageTabName {
101101
MODEL_METRIC_CHARTS = 'model-metrics',
102102
SYSTEM_METRIC_CHARTS = 'system-metrics',
103103
ARTIFACTS = 'artifacts',
104+
STDOUT = 'stdout',
104105
EVALUATIONS = 'evaluations',
105106
}
106107

stdout_tab_guide.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# MLflow Stdout Tab Guide
2+
3+
The stdout tab functionality is already implemented in MLflow! This guide shows you how to use it.
4+
5+
## How It Works
6+
7+
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.
8+
9+
## Backend Usage
10+
11+
### Basic Usage
12+
13+
```python
14+
import mlflow
15+
16+
with mlflow.start_run(log_stdout=True):
17+
print("This will appear in the stdout tab!")
18+
# Your code here
19+
```
20+
21+
### With Custom Interval
22+
23+
```python
24+
import mlflow
25+
26+
# Log stdout every 3 seconds instead of default 5
27+
with mlflow.start_run(log_stdout=True, log_stdout_interval=3):
28+
print("This will appear in the stdout tab!")
29+
# Your code here
30+
```
31+
32+
## Frontend Features
33+
34+
The stdout tab includes:
35+
36+
- **Syntax highlighting** for better readability
37+
- **Line numbers** for easy navigation
38+
- **Dark/light theme** support
39+
- **Auto-scrolling** and text wrapping
40+
- **Error handling** for missing or empty stdout logs
41+
42+
## Tab Location
43+
44+
The stdout tab appears in the run details page alongside:
45+
46+
- Overview
47+
- Model metrics
48+
- System metrics
49+
- **Stdout** ← Here!
50+
- Artifacts
51+
52+
## Testing
53+
54+
Run the test script to verify everything works:
55+
56+
```bash
57+
python test_stdout_tab.py
58+
```
59+
60+
Then:
61+
62+
1. Open MLflow UI (usually http://localhost:5000)
63+
2. Navigate to the test run
64+
3. Click the "Stdout" tab
65+
4. You should see all the captured output!
66+
67+
## Implementation Details
68+
69+
### Backend Files
70+
71+
- `mlflow/utils/stdout_logging.py` - Core stdout capture logic
72+
- `mlflow/tracking/fluent.py` - Integration with start_run()
73+
74+
### Frontend Files
75+
76+
- `mlflow/server/js/src/experiment-tracking/components/run-page/RunViewStdoutTab.tsx` - Tab component
77+
- `mlflow/server/js/src/experiment-tracking/components/run-page/RunViewModeSwitch.tsx` - Tab switcher
78+
- `mlflow/server/js/src/experiment-tracking/constants.ts` - Tab definitions
79+
80+
## Similar to Wandb
81+
82+
This provides similar functionality to Wandb's stdout logging, where you can:
83+
84+
- View real-time stdout output
85+
- Navigate through logs easily
86+
- Keep logs organized per run
87+
- Access logs directly from the UI
88+
89+
The stdout tab is ready to use - no additional setup required!

0 commit comments

Comments
 (0)