Skip to content

Commit

Permalink
Add support for log groups
Browse files Browse the repository at this point in the history
Update log viewer to support `::group::` and `::endgroup::` commands
which create collapsible groups in the log content.

When logs of a running step are viewed, groups are rendered in their
expanded state by default. When logs of a completed step are viewed,
groups are rendered in their collapsed state by default.

In either case, if a user manually toggles a group, that state is
respected until they switch to another view, regardless of the running
state of the step.
  • Loading branch information
AlanGreene authored and tekton-robot committed Jan 20, 2025
1 parent 94ef989 commit cc9ab72
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 23 deletions.
27 changes: 27 additions & 0 deletions docs/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ echo '::warning::Something that may require attention but is non-blocking…'

The displayed log levels can be changed via the settings menu in the toolbar at the top of the log viewer.

### Log groups

In addition to log levels, the log viewer also supports collapsible groups within the logs. The format supported is described below.

```
<timestamp> ::group::<message>
<timestamp> ::endgroup::
```

A `group` command marks the beginning of the group. The content of `message` is displayed as the title / summary of the group along with an indicator of the group's current state (i.e. expanded or collapsed). Clicking the summary will toggle the state of the group.

Groups are rendered in the collapsed state by default unless the step is still in progress when the logs are viewed. The user can expand or collapse groups as desired and their state will be maintained until the user navigates to a different view.

Log groups cannot be mixed with log levels on the same line, the `group`, `endgroup`, and log level commands are mutually exclusive. However, logs contained within a group can use log levels as normal.

Nesting groups is not supported. A `group` command will implicitly terminate any prior unterminated group.

For example, the following snippet would output a log group with the summary 'Additional config' containing a number of messages at the `info` level:

```sh
echo '::group::Additional config'
echo 'This extends the base config'
echo '::info:: More info about the config…'
echo '::endgroup::'
```

## Logs persistence

By default, Tekton Dashboard loads the logs from the Kubernetes API server, using the pod logs API. However, it also supports loading logs from an external source when the container logs or the pods associated with the `TaskRuns` are no longer available on the cluster.
Expand Down
68 changes: 61 additions & 7 deletions packages/components/src/components/Log/Log.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* istanbul ignore file */

import { Component, createRef } from 'react';
import { Button, PrefixContext, SkeletonText } from '@carbon/react';
Expand All @@ -31,7 +32,7 @@ const itemSize = 16; // This should be kept in sync with the line-height in SCSS
const defaultHeight = itemSize * 100 + itemSize / 2;

const logFormatRegex =
/^((?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3,9}Z)\s?)?(::(?<level>error|warning|info|notice|debug)::)?(?<message>.*)?$/s;
/^((?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3,9}Z)\s?)?(::(?<command>group|endgroup|error|warning|info|notice|debug)::)?(?<message>.*)?$/s;

function LogsFilteredNotification({ displayedLogLines, totalLogLines }) {
const intl = useIntl();
Expand All @@ -46,7 +47,8 @@ function LogsFilteredNotification({ displayedLogLines, totalLogLines }) {
<Information />
{intl.formatMessage({
id: 'dashboard.logs.hidden.all',
defaultMessage: 'All lines hidden due to selected log levels'
defaultMessage:
'All lines hidden due to selected log levels or collapsed groups'
})}
</span>
);
Expand All @@ -58,15 +60,16 @@ function LogsFilteredNotification({ displayedLogLines, totalLogLines }) {
? intl.formatMessage(
{
id: 'dashboard.logs.hidden.one',
defaultMessage: '1 line hidden due to selected log levels'
defaultMessage:
'1 line hidden due to selected log levels or collapsed groups'
},
{ numHiddenLines: totalLogLines - displayedLogLines }
)
: intl.formatMessage(
{
id: 'dashboard.logs.hidden',
defaultMessage:
'{numHiddenLines, plural, other {# lines}} hidden due to selected log levels'
'{numHiddenLines, plural, other {# lines}} hidden due to selected log levels or collapsed groups'
},
{ numHiddenLines: totalLogLines - displayedLogLines }
);
Expand All @@ -82,7 +85,7 @@ function LogsFilteredNotification({ displayedLogLines, totalLogLines }) {
export class LogContainer extends Component {
constructor(props) {
super(props);
this.state = { loading: true, logs: [] };
this.state = { groupsExpanded: {}, loading: true, logs: [] };
this.logRef = createRef();
this.textRef = createRef();
}
Expand Down Expand Up @@ -116,6 +119,15 @@ export class LogContainer extends Component {
this.cancelled = true;
}

onToggleGroup = ({ expanded, groupIndex }) => {
this.setState(({ groupsExpanded }) => ({
groupsExpanded: {
...groupsExpanded,
[groupIndex]: expanded
}
}));
};

handleLogScroll = () => {
if (!this.state.loading) {
const isLogBottomUnseen = this.isLogBottomUnseen();
Expand Down Expand Up @@ -296,9 +308,14 @@ export class LogContainer extends Component {
}

const {
groups: { level, message, timestamp }
groups: { command, message, timestamp }
} = logFormatRegex.exec(line);
let level;
if (!['group', 'endgroup'].includes(command)) {
level = command;
}
return {
command,
level,
message,
timestamp
Expand All @@ -310,6 +327,7 @@ export class LogContainer extends Component {
} = this.props;
const { reason } = (stepStatus && stepStatus.terminated) || {};
const {
groupsExpanded,
logs = [
intl.formatMessage({
id: 'dashboard.pipelineRun.logEmpty',
Expand All @@ -319,7 +337,8 @@ export class LogContainer extends Component {
} = this.state;

let previousTimestamp;
const parsedLogs = logs.reduce((acc, line) => {
let currentGroupIndex = null;
const parsedLogs = logs.reduce((acc, line, index) => {
const parsedLogLine = parseLogLine(line);
if (!parsedLogLine.timestamp) {
// multiline log, use same timestamp as previous line
Expand All @@ -328,6 +347,39 @@ export class LogContainer extends Component {
previousTimestamp = parsedLogLine.timestamp;
}

const isGroup = parsedLogLine.command === 'group';
const isEndGroup = parsedLogLine.command === 'endgroup';
if (isGroup) {
currentGroupIndex = index;
parsedLogLine.groupIndex = index;
if (groupsExpanded[index] !== false) {
// if not already explicitly collapsed,
// respect state if explicitly expanded, otherwise
// should be expanded by default if viewed when step running
// or collapsed by default if viewing after run complete
parsedLogLine.expanded =
groupsExpanded[index] || this.wasRunningAfterMounting();
}
} else if (isEndGroup) {
currentGroupIndex = null;
// we don't render anything for the endgroup command
return acc;
}

if (!isGroup && currentGroupIndex !== null) {
// we're inside a group, determine if the line should be rendered
if (
groupsExpanded[currentGroupIndex] === false ||
(!groupsExpanded[currentGroupIndex] &&
!this.wasRunningAfterMounting())
) {
// skip if either explicitly collapsed, or viewed after step completed so collapsed by default
return acc;
}

parsedLogLine.groupIndex = currentGroupIndex;
}

if (
!logLevels ||
// we treat lines with no log level as if they specified 'info'
Expand All @@ -351,6 +403,7 @@ export class LogContainer extends Component {
<LogFormat
fields={{ level: showLevels, timestamp: showTimestamps }}
logs={parsedLogs}
onToggleGroup={this.onToggleGroup}
/>
</>
);
Expand Down Expand Up @@ -378,6 +431,7 @@ export class LogContainer extends Component {
<LogFormat
fields={{ level: showLevels, timestamp: showTimestamps }}
logs={[data[index]]}
onToggleGroup={this.onToggleGroup}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
2024-11-14T14:11:42.498953257Z For more information on the stages that can be skipped refer to the doc link below
2024-11-14T14:11:42.498957672Z https://cloud.ibm.com/docs/devsecops?topic=devsecops-cd-devsecops-pr-pipeline
2024-11-14T14:11:42.498961928Z
2024-11-14T14:11:42.498964925Z
2024-11-14T14:11:42.498964925Z ::group::Process repository parameters
2024-11-14T14:11:43.342596128Z ::debug::[set-commit-status:48] | repository: https://github.com/example-org/example-app
2024-11-14T14:11:43.342933970Z ::debug::[set-commit-status:60] | commit-sha: b00b89b53ead4344a55d003b1f5698183e6128ab
2024-11-14T14:11:43.342946722Z ::debug::[set-commit-status:72] | state: pending
Expand Down Expand Up @@ -112,6 +112,7 @@
2024-11-14T14:11:43.750947704Z ::debug::[get_repo_params:167] | Unable to retreive Personal Access Token. Attempt to fetch token from Toolchain Broker.
2024-11-14T14:11:43.782406568Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github
2024-11-14T14:11:44.375617191Z ::debug::[get_credentials_v2:187] | Fetch Git Token for SCM Type: github, SCM ID: integrated, Repository URL: https://github.com/example-org/example-app.
2024-11-14T14:11:44.375617191Z ::endgroup::
2024-11-14T14:11:44.415251742Z ::info::Fetch git token for https://github.com/example-org/example-inventory.git
2024-11-14T14:11:44.507177971Z ::debug::[get_credentials_v2:106] | Returning git token for https://github.com/example-org/example-inventory.git as found in cache.
2024-11-14T14:11:44.825013004Z ::debug::[set-commit-status:219] | Calling set-commit-status with params %s: --state=pending --targetURL=https://cloud.ibm.com/devops/pipelines/tekton/a3fe08ad-d5de-4cd2-98b0-f5c5219a33cd/runs/5f3949cf-de85-415c-a6cc-74a336bbe206/code-unit-tests/run-stage?env_id=ibm:yp:us-south --context=tekton/code-unit-tests --description=Running unit tests... --git-provider=github --git-token-path=/workspace/app/secrets/app-token --git-api-url=https://github.com/api/v3
Expand Down
36 changes: 31 additions & 5 deletions packages/components/src/components/LogFormat/LogFormat.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020-2024 The Tekton Authors
Copyright 2020-2025 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Expand All @@ -10,6 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* istanbul ignore file */

import tlds from 'tlds';
import LinkifyIt from 'linkify-it';
Expand Down Expand Up @@ -92,7 +93,11 @@ const linkify = (str, styleObj, classNameString) => {
return elements;
};

const LogFormat = ({ fields = { message: true }, logs = [] }) => {
const LogFormat = ({
fields = { message: true },
logs = [],
onToggleGroup
}) => {
let properties = {
classes: {},
foregroundColor: null,
Expand Down Expand Up @@ -249,7 +254,14 @@ const LogFormat = ({ fields = { message: true }, logs = [] }) => {
};

const parse = (log, index) => {
const { level, message = '', timestamp } = log;
const {
command,
expanded,
groupIndex = null,
level,
message = '',
timestamp
} = log;
if (!message?.length && !timestamp && !level) {
return <br key={index} />;
}
Expand Down Expand Up @@ -283,7 +295,9 @@ const LogFormat = ({ fields = { message: true }, logs = [] }) => {
return (
<div
className={classNames('tkn--log-line', {
[`tkn--log-level--${level}`]: level
[`tkn--log-level--${level}`]: level,
'tkn--log-line--group': command === 'group',
'tkn--log-line--in-group': command !== 'group' && groupIndex !== null
})}
key={index}
>
Expand All @@ -297,7 +311,19 @@ const LogFormat = ({ fields = { message: true }, logs = [] }) => {
</span>
)}
{fields.level && getDecoratedLevel(level)}
{line}
{command === 'group' && (
<details
onToggle={event =>
onToggleGroup({ expanded: event.target.open, groupIndex })
}
open={expanded}
>
<summary>{line}</summary>
</details>
)}
{!['group', 'endgroup'].includes(command) && (
<span className="tkn--log-line--content">{line}</span>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020-2024 The Tekton Authors
Copyright 2020-2025 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Expand Down Expand Up @@ -148,6 +148,35 @@ export const LogLevelsAndTimestamps = {
timestamp: '2024-11-14T14:10:56.869719012Z',
level: 'notice',
message: 'Sample notice'
},
{
timestamp: '2024-11-14T14:10:56.869719012Z',
command: 'group',
expanded: false,
message: 'Collapsed group'
},
{
timestamp: '2024-11-14T14:10:56.869719012Z',
command: 'group',
expanded: true,
message: 'Expanded group'
},
{
timestamp: '2024-11-14T14:10:56.869719012Z',
level: 'info',
isInGroup: true,
message: 'First line inside group'
},
{
timestamp: '2024-11-14T14:10:56.869719012Z',
level: 'debug',
isInGroup: true,
message: 'Second line inside group'
},
{
timestamp: '2024-11-14T14:10:56.869719012Z',
isInGroup: true,
message: 'A line with no log level inside a group'
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020-2024 The Tekton Authors
Copyright 2020-2025 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Expand Down Expand Up @@ -321,7 +321,7 @@ describe('LogFormat', () => {
const logs = [{ message: 'Hello' }, { message: '' }, { message: 'World' }];
const { container } = render(<LogFormat logs={logs} />);
expect(container.childNodes[0].innerHTML).toBe(
'<div class="tkn--log-line">Hello</div><br><div class="tkn--log-line">World</div>'
'<div class="tkn--log-line"><span class="tkn--log-line--content">Hello</span></div><br><div class="tkn--log-line"><span class="tkn--log-line--content">World</span></div>'
);
});

Expand Down Expand Up @@ -352,7 +352,7 @@ describe('LogFormat', () => {
<LogFormat fields={{ timestamp: false }} logs={logs} />
);
expect(container.childNodes[0].innerHTML).toBe(
'<div class="tkn--log-line">Hello</div>'
'<div class="tkn--log-line"><span class="tkn--log-line--content">Hello</span></div>'
);
rerender(<LogFormat fields={{ timestamp: true }} logs={logs} />);
expect(container.childNodes[0].innerHTML).toMatch(
Expand All @@ -361,7 +361,7 @@ describe('LogFormat', () => {
// accept anything (`.*`) for test purposes as it may be localised
// and we're more concerned with the structure here
new RegExp(
`<div class="tkn--log-line"><span class="tkn--log-line--timestamp"><span title="${timestamp}">.*</span></span>Hello</div>`
`<div class="tkn--log-line"><span class="tkn--log-line--timestamp"><span title="${timestamp}">.*</span></span><span class="tkn--log-line--content">Hello</span></div>`
)
);
});
Expand Down
22 changes: 21 additions & 1 deletion packages/components/src/components/LogFormat/_LogFormat.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020-2024 The Tekton Authors
Copyright 2020-2025 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Expand Down Expand Up @@ -117,6 +117,26 @@ $colors: (
font-weight: 300;
}

&.tkn--log-line--group {
details {
display: inline-block;
}
}

&.tkn--log-line--in-group {
// indent log content inside an expanded group
// account for lines with no explicit log level
.tkn--log-line--level,
.tkn--log-line--content {
margin-inline-start: 0.5rem;
}

// reset for lines with log level so we only apply indent on log level
.tkn--log-line--level + .tkn--log-line--content {
margin-inline-start: 0;
}
}

.tkn--log-line--level {
display: inline-block;
padding-inline: 4px;
Expand Down
Loading

0 comments on commit cc9ab72

Please sign in to comment.