Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));
Expand All @@ -83,23 +84,42 @@ 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' }));

expect(screen.queryByText('overview tab')).not.toBeInTheDocument();
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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -140,6 +141,14 @@ export const RunPage = () => {
artifactUri={runInfo.artifactUri ?? undefined}
/>
);
case RunPageTabName.STDOUT:
return (
<RunViewStdoutTab
runUuid={runUuid}
experimentId={experimentId}
runTags={tags}
/>
);
case RunPageTabName.TRACES:
if (shouldEnableRunDetailsPageTracesTab()) {
return <RunViewTracesTab runUuid={runUuid} runTags={tags} experimentId={experimentId} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export const RunViewModeSwitch = () => {
key={RunPageTabName.SYSTEM_METRIC_CHARTS}
/>
{getLegacyTracesTabLink()}
<LegacyTabs.TabPane
tab={
<FormattedMessage defaultMessage="Stdout" description="Run details page > tab selector > Stdout tab" />
}
key={RunPageTabName.STDOUT}
/>
<LegacyTabs.TabPane
tab={
<FormattedMessage defaultMessage="Artifacts" description="Run details page > tab selector > artifacts tab" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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<string, KeyValueEntity>;
}) => {
const { theme } = useDesignSystemTheme();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [stdoutContent, setStdoutContent] = useState<string>('');

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 (
<div css={{ flex: 1, padding: theme.spacing.md }}>
<ArtifactViewSkeleton className="stdout-loading" />
</div>
);
}

if (error) {
return (
<div css={{ flex: 1, padding: theme.spacing.md }}>
<Empty
image={<DangerIcon />}
title={
<FormattedMessage
defaultMessage="No stdout logs found"
description="Run page > stdout tab > no stdout logs title"
/>
}
description={
<FormattedMessage
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()."
description="Run page > stdout tab > no stdout logs description"
/>
}
/>
</div>
);
}

if (!stdoutContent.trim()) {
return (
<div css={{ flex: 1, padding: theme.spacing.md }}>
<Empty
title={
<FormattedMessage
defaultMessage="No stdout output"
description="Run page > stdout tab > empty stdout title"
/>
}
description={
<FormattedMessage
defaultMessage="The stdout.log file exists but contains no content."
description="Run page > stdout tab > empty stdout description"
/>
}
/>
</div>
);
}

const syntaxStyle = theme.isDarkMode ? darkStyle : style;

return (
<div
css={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
padding: theme.spacing.md,
}}
>
<div css={{ marginBottom: theme.spacing.sm }}>
<Typography.Title level={3}>
<FormattedMessage
defaultMessage="Standard Output"
description="Run page > stdout tab > title"
/>
</Typography.Title>
<Typography.Text color="secondary">
<FormattedMessage
defaultMessage="Real-time stdout logs captured during run execution"
description="Run page > stdout tab > subtitle"
/>
</Typography.Text>
</div>

<div
css={{
flex: 1,
overflow: 'auto',
border: `1px solid ${theme.colors.borderDecorative}`,
borderRadius: theme.borders.borderRadiusMd,
}}
>
<SyntaxHighlighter
language="text"
style={syntaxStyle}
customStyle={{
fontFamily: 'Source Code Pro, Menlo, Monaco, monospace',
fontSize: theme.typography.fontSizeSm,
margin: 0,
padding: theme.spacing.md,
backgroundColor: theme.colors.backgroundSecondary,
whiteSpace: 'pre',
height: '100%',
overflow: 'auto',
}}
showLineNumbers={false}
wrapLongLines={false}
>
{stdoutContent}
</SyntaxHighlighter>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions mlflow/server/js/src/experiment-tracking/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export enum RunPageTabName {
MODEL_METRIC_CHARTS = 'model-metrics',
SYSTEM_METRIC_CHARTS = 'system-metrics',
ARTIFACTS = 'artifacts',
STDOUT = 'stdout',
EVALUATIONS = 'evaluations',
}

Expand Down
89 changes: 89 additions & 0 deletions stdout_tab_guide.md
Original file line number Diff line number Diff line change
@@ -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!