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
6 changes: 5 additions & 1 deletion docs/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,18 @@ It can be enabled by providing the `--external-logs` flag to the installer scrip
When configured, the Dashboard will first attempt to load pod logs normally, and if they're unavailable will fallback to the provided external logs service by making a `GET` request to the provided endpoint with the following format:

```
GET <external-logs>/<namespace>/<podName>/<container>?startTime=<stepStartTime>&completionTime=<stepCompletionTime>
GET <external-logs>/<namespace>/<podName>/<container>?startTime=<stepStartTime>&completionTime=<stepCompletionTime>&timestamps=<true|false>&logLevel=<selectedLogLevel>&logLevel=<selectedLogLevel>
```

- `namespace`: the namespace containing the run
- `podName`: the name of the `Pod` resource associated with the selected `TaskRun`
- `container`: the name of the container associated with the selected `step`
- `stepStartTime`: the start time of the step container
- `stepCompletionTime`: the completion time of the step container
- `timestamps`: the user's timestamp preference
- `logLevel`: repeated for each user-selected log level

Only `namespace`, `podName`, and `container` are required. Support for all other parameters is optional and they may be ignored by external log providers.

If the start / completion times are unavailable their respective query parameters will be omitted from the request.

Expand Down
12 changes: 11 additions & 1 deletion src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2025 The Tekton Authors
Copyright 2019-2026 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 @@ -124,8 +124,10 @@ export function getExternalLogURL({
completionTime,
container,
externalLogsURL,
logLevels,
namespace,
podName,
showTimestamps,
startTime
}) {
const queryParams = new URLSearchParams();
Expand All @@ -135,6 +137,14 @@ export function getExternalLogURL({
if (completionTime) {
queryParams.set('completionTime', completionTime);
}
if (showTimestamps !== undefined) {
queryParams.set('timestamps', showTimestamps);
}
Object.entries(logLevels || {}).forEach(([logLevel, enabled]) => {
if (enabled) {
queryParams.append('logLevel', logLevel);
}
});
let queryString = queryParams.toString(); // returns the properly encoded string, or '' if no params
if (queryString) {
queryString = `?${queryString}`;
Expand Down
26 changes: 25 additions & 1 deletion src/api/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2025 The Tekton Authors
Copyright 2019-2026 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 @@ -138,6 +138,30 @@ it('getExternalLogURL', () => {
)}&completionTime=${completionTime.replaceAll(':', '%3A')}`
);
});
it('getExternalLogURL includes log preferences', () => {
const container = 'fake_container';
const externalLogsURL = '/fake_externalLogsURL';
const logLevels = {
error: true,
info: true,
warning: false
};
const namespace = 'fake_namespace';
const podName = 'fake_podName';
const showTimestamps = false;
expect(
API.getExternalLogURL({
container,
externalLogsURL,
logLevels,
namespace,
podName,
showTimestamps
})
).toEqual(
`http://localhost:3000${externalLogsURL}/${namespace}/${podName}/${container}?timestamps=false&logLevel=error&logLevel=info`
);
});

it('getExternalLogURL with empty completionTime', () => {
const container = 'fake_container';
Expand Down
16 changes: 12 additions & 4 deletions src/containers/PipelineRun/PipelineRun.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -521,23 +521,31 @@ export /* istanbul ignore next */ function PipelineRunContainer({
fetchLogs={getLogsRetriever({
externalLogsURL,
isLogStreamingEnabled,
onFallback: setIsUsingExternalLogs
logLevels,
onFallback: setIsUsingExternalLogs,
showTimestamps
})}
handleTaskSelected={handleTaskSelected}
loading={isLoading}
logLevels={logLevels}
getLogsToolbar={toolbarProps => (
<LogsToolbar
{...toolbarProps}
externalLogsURL={externalLogsURL}
isUsingExternalLogs={isUsingExternalLogs}
logLevels={showLogLevels && logLevels}
onToggleLogLevel={onToggleLogLevel}
onToggleShowTimestamps={onToggleShowTimestamps}
showTimestamps={showTimestamps}
/>
)}
getStepLogToolbar={toolbarProps => <StepLogToolbar {...toolbarProps} />}
getStepLogToolbar={toolbarProps => (
<StepLogToolbar
{...toolbarProps}
externalLogsURL={externalLogsURL}
isUsingExternalLogs={isUsingExternalLogs}
logLevels={logLevels}
showTimestamps={showTimestamps}
/>
)}
onRetryChange={retry => {
if (Number.isInteger(retry)) {
queryParams.set(RETRY, retry);
Expand Down
11 changes: 10 additions & 1 deletion src/containers/StepLogToolbar/StepLogToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { getExternalLogURL, getPodLogURL } from '../../api';
export default function StepLogToolbarContainer({
externalLogsURL,
isUsingExternalLogs,
logLevels,
showTimestamps,
stepStatus,
taskRun
}) {
Expand All @@ -28,7 +30,14 @@ export default function StepLogToolbarContainer({
let logURL;
if (container && podName) {
logURL = isUsingExternalLogs
? getExternalLogURL({ container, externalLogsURL, namespace, podName })
? getExternalLogURL({
container,
externalLogsURL,
logLevels,
namespace,
podName,
showTimestamps
})
: getPodLogURL({
container,
name: podName,
Expand Down
8 changes: 7 additions & 1 deletion src/containers/StepLogToolbar/StepLogToolbar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ describe('getLogsToolbar', () => {
it('should handle external logs', () => {
const container = 'fake_container';
const externalLogsURL = 'fake_externalLogsURL';
const logLevels = { error: true };
const showTimestamps = false;
const namespace = 'fake_namespace';
const podName = 'fake_podname';
const stepStatus = { container };
Expand All @@ -50,6 +52,8 @@ describe('getLogsToolbar', () => {
<LogsToolbarContainer
externalLogsURL={externalLogsURL}
isUsingExternalLogs
logLevels={logLevels}
showTimestamps={showTimestamps}
stepStatus={stepStatus}
taskRun={taskRun}
/>
Expand All @@ -59,8 +63,10 @@ describe('getLogsToolbar', () => {
expect(API.getExternalLogURL).toHaveBeenCalledWith({
container,
externalLogsURL,
logLevels,
namespace,
podName
podName,
showTimestamps
});
});

Expand Down
6 changes: 5 additions & 1 deletion src/containers/TaskRun/TaskRun.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ export function TaskRunContainer({
const logsRetriever = getLogsRetriever({
externalLogsURL,
isLogStreamingEnabled,
onFallback: setIsUsingExternalLogs
logLevels,
onFallback: setIsUsingExternalLogs,
showTimestamps
});

return (
Expand All @@ -184,6 +186,8 @@ export function TaskRunContainer({
<StepLogToolbar
externalLogsURL={externalLogsURL}
isUsingExternalLogs={isUsingExternalLogs}
logLevels={logLevels}
showTimestamps={showTimestamps}
stepStatus={stepStatus}
taskRun={run}
/>
Expand Down
18 changes: 15 additions & 3 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ export async function fetchLogs({ _stepName, stream, stepStatus, taskRun }) {
return logs;
}

export function fetchLogsFallback(externalLogsURL) {
export function fetchLogsFallback({
externalLogsURL,
logLevels,
showTimestamps
}) {
if (!externalLogsURL) {
return undefined;
}
Expand All @@ -132,8 +136,10 @@ export function fetchLogsFallback(externalLogsURL) {
getExternalLogURL({
container,
externalLogsURL,
logLevels,
namespace,
podName,
showTimestamps,
startTime,
completionTime
}),
Expand All @@ -147,9 +153,15 @@ export function fetchLogsFallback(externalLogsURL) {
export function getLogsRetriever({
externalLogsURL,
isLogStreamingEnabled,
onFallback
logLevels,
onFallback,
showTimestamps
}) {
const fallback = fetchLogsFallback(externalLogsURL);
const fallback = fetchLogsFallback({
externalLogsURL,
logLevels,
showTimestamps
});

if (fallback) {
return ({ stepName, stepStatus, taskRun }) =>
Expand Down
46 changes: 41 additions & 5 deletions src/utils/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,34 @@ describe('fetchLogs', () => {

describe('fetchLogsFallback', () => {
it('should return undefined when no external log provider configured', () => {
expect(fetchLogsFallback()).toBeUndefined();
expect(fetchLogsFallback({})).toBeUndefined();
});

it('should pass preferences to the external log provider', () => {
const container = 'fake_container';
const externalLogsURL = '/fake_url';
const logLevels = {
error: true,
info: false
};
const namespace = 'fake_namespace';
const podName = 'fake_podName';
const stepName = 'fake_stepName';
const showTimestamps = true;
const stepStatus = { container };
const taskRun = { metadata: { namespace }, status: { podName } };
vi.spyOn(comms, 'get').mockImplementation(() => {});

const fallback = fetchLogsFallback({
externalLogsURL,
logLevels,
showTimestamps
});
fallback({ stepName, stepStatus, taskRun });
expect(comms.get).toHaveBeenCalledWith(
`http://localhost:3000${externalLogsURL}/${namespace}/${podName}/${container}?timestamps=true&logLevel=error`,
{ Accept: 'text/plain' }
);
});

it('should return a function to retrieve logs from the external provider', () => {
Expand All @@ -217,7 +244,7 @@ describe('fetchLogsFallback', () => {
};
vi.spyOn(comms, 'get').mockImplementation(() => {});

const fallback = fetchLogsFallback(externalLogsURL);
const fallback = fetchLogsFallback({ externalLogsURL });
fallback({ stepName, stepStatus, taskRun });
expect(comms.get).toHaveBeenCalledWith(
`http://localhost:3000${externalLogsURL}/${namespace}/${podName}/${container}?startTime=${startTime.replaceAll(
Expand All @@ -238,7 +265,7 @@ describe('fetchLogsFallback', () => {
const taskRun = { metadata: { namespace }, status: { podName } };
vi.spyOn(comms, 'get').mockImplementation(() => {});

const fallback = fetchLogsFallback(externalLogsURL);
const fallback = fetchLogsFallback({ externalLogsURL });
fallback({ stepName, stepStatus, taskRun });
expect(comms.get).toHaveBeenCalledWith(
`http://localhost:3000${externalLogsURL}/${namespace}/${podName}/${container}`,
Expand Down Expand Up @@ -296,21 +323,30 @@ describe('getLogsRetriever', () => {

it('should handle external logs fallback', async () => {
const externalLogsURL = 'fake_externalLogsURL';
const logLevels = { error: true };
const showTimestamps = false;
vi.spyOn(API, 'getExternalLogURL');
vi.spyOn(API, 'getPodLog').mockImplementation(() => {
throw new Error();
});
vi.spyOn(comms, 'get').mockImplementation(() => {});
const onFallback = vi.fn();
const logsRetriever = getLogsRetriever({ externalLogsURL, onFallback });
const logsRetriever = getLogsRetriever({
externalLogsURL,
logLevels,
onFallback,
showTimestamps
});
expect(logsRetriever).toBeDefined();
await logsRetriever({ stepName, stepStatus, taskRun });
expect(API.getPodLog).toHaveBeenCalledWith({
container: stepName,
name: podName,
namespace
});
expect(API.getExternalLogURL).toHaveBeenCalled();
expect(API.getExternalLogURL).toHaveBeenCalledWith(
expect.objectContaining({ logLevels, showTimestamps })
);
expect(onFallback).toHaveBeenCalledWith(true);
});
});
Expand Down
Loading