-
Notifications
You must be signed in to change notification settings - Fork 16.4k
Add button to download task logs as a text file #49412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -155,3 +155,138 @@ export const useLogs = ( | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { data: parsedData, ...rest }; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| type LineObject = { | ||||||||||||||||||||||||||||||||||||||
| props?: Props; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const logDateTime = (line: string): string | undefined => { | ||||||||||||||||||||||||||||||||||||||
| if (!line || typeof line !== "object") { | ||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const lineObj = line as LineObject; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!lineObj.props || !("children" in lineObj.props)) { | ||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { children } = lineObj.props; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(children) || children.length <= 2) { | ||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { 2: thirdChild } = children; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const thirdChildObj = thirdChild as { props?: { datetime?: string } }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!thirdChildObj.props || typeof thirdChildObj.props.datetime !== "string") { | ||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const datetimeStr = thirdChildObj.props.datetime; | ||||||||||||||||||||||||||||||||||||||
| const date = new Date(datetimeStr); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (isNaN(date.getTime())) { | ||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const year = date.getFullYear(); | ||||||||||||||||||||||||||||||||||||||
| const month = date.getMonth() + 1; | ||||||||||||||||||||||||||||||||||||||
| const day = date.getDate(); | ||||||||||||||||||||||||||||||||||||||
| const hours = date.getHours(); | ||||||||||||||||||||||||||||||||||||||
| const minutes = date.getMinutes(); | ||||||||||||||||||||||||||||||||||||||
| const seconds = date.getSeconds(); | ||||||||||||||||||||||||||||||||||||||
| const formattedDate = `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`; | ||||||||||||||||||||||||||||||||||||||
| const formattedTime = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return `${formattedDate}, ${formattedTime}`; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const logText = ({ data, logLevelFilters, sourceFilters, taskInstance, tryNumber }: ParseLogsProps) => { | ||||||||||||||||||||||||||||||||||||||
| let warning; | ||||||||||||||||||||||||||||||||||||||
| let parsedLines; | ||||||||||||||||||||||||||||||||||||||
| const sources: Array<string> = []; | ||||||||||||||||||||||||||||||||||||||
| const logLink = taskInstance ? `${getTaskInstanceLink(taskInstance)}?try_number=${tryNumber}` : ""; | ||||||||||||||||||||||||||||||||||||||
| const elements: Array<string> = []; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| parsedLines = data.map((datum, index) => { | ||||||||||||||||||||||||||||||||||||||
| if (typeof datum !== "string" && "logger" in datum) { | ||||||||||||||||||||||||||||||||||||||
| const source = datum.logger as string; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!sources.includes(source)) { | ||||||||||||||||||||||||||||||||||||||
| sources.push(source); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return renderStructuredLog({ index, logLevelFilters, logLink, logMessage: datum, sourceFilters }); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| const errorMessage = error instanceof Error ? error.message : "An error occurred."; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| console.warn(`Error parsing logs: ${errorMessage}`); | ||||||||||||||||||||||||||||||||||||||
| warning = "Unable to show logs. There was an error parsing logs."; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { data, warning }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| parsedLines.map((line) => { | ||||||||||||||||||||||||||||||||||||||
| const text = innerText(line); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (text !== "") { | ||||||||||||||||||||||||||||||||||||||
| const datetime = logDateTime(line as string); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (datetime === undefined) { | ||||||||||||||||||||||||||||||||||||||
| elements.push(`${text}\n`); | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| const first = text.slice(0, Math.max(0, text.indexOf("["))); | ||||||||||||||||||||||||||||||||||||||
| const second = text.slice(Math.max(0, text.indexOf("[") + 1)); | ||||||||||||||||||||||||||||||||||||||
| const newtext = `${first}[${datetime}${second}`; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| elements.push(`${newtext}\n`); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return text; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return elements; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export const useLogDownload = ( | ||||||||||||||||||||||||||||||||||||||
| { dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }: Props, | ||||||||||||||||||||||||||||||||||||||
| options?: Omit<UseQueryOptions<TaskInstancesLogResponse>, "queryFn" | "queryKey">, | ||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||
| const refetchInterval = useAutoRefresh({ dagId }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { data, ...rest } = useTaskInstanceServiceGetLog( | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you can pass
airflow/airflow-core/src/airflow/api_fastapi/core_api/routes/public/log.py Lines 136 to 153 in 96c6daa
|
||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| dagId, | ||||||||||||||||||||||||||||||||||||||
| dagRunId: taskInstance?.dag_run_id ?? "", | ||||||||||||||||||||||||||||||||||||||
| mapIndex: taskInstance?.map_index ?? -1, | ||||||||||||||||||||||||||||||||||||||
| taskId: taskInstance?.task_id ?? "", | ||||||||||||||||||||||||||||||||||||||
| tryNumber, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| undefined, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| enabled: Boolean(taskInstance), | ||||||||||||||||||||||||||||||||||||||
| refetchInterval: (query) => | ||||||||||||||||||||||||||||||||||||||
| isStatePending(taskInstance?.state) || | ||||||||||||||||||||||||||||||||||||||
| dayjs(query.state.dataUpdatedAt).isBefore(taskInstance?.end_date) | ||||||||||||||||||||||||||||||||||||||
| ? refetchInterval | ||||||||||||||||||||||||||||||||||||||
| : false, | ||||||||||||||||||||||||||||||||||||||
| ...options, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const logs = logText({ | ||||||||||||||||||||||||||||||||||||||
| data: data?.content ?? [], | ||||||||||||||||||||||||||||||||||||||
| logLevelFilters, | ||||||||||||||||||||||||||||||||||||||
| sourceFilters, | ||||||||||||||||||||||||||||||||||||||
| taskInstance, | ||||||||||||||||||||||||||||||||||||||
| tryNumber, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { datum: logs, ...rest }; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks the filename for the downloaded log file. This should include dagId and taskInstance details in the name like Airflow 2 else this might keep overwriting the file on disk or create something like
taskInstanceLogs(1).txtUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on
${dagId}-${taskId}-${runId}-${mapIndex}-${tryNumber}.txt