Skip to content

Commit

Permalink
Migrate task instance log (ti_log) js (apache#15309)
Browse files Browse the repository at this point in the history
* migrate ti_log js

* fix wrap and toggle buttons
  • Loading branch information
bbovenzi authored Apr 9, 2021
1 parent bcc6c93 commit 9dd14aa
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 125 deletions.
148 changes: 148 additions & 0 deletions airflow/www/static/js/ti_log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, 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.
*/

/* global document, window, $, */
import { escapeHtml } from './main';
import getMetaValue from './meta_value';

const executionDate = getMetaValue('execution_date');
const dagId = getMetaValue('dag_id');
const taskId = getMetaValue('task_id');
const logsWithMetadataUrl = getMetaValue('logs_with_metadata_url');
const DELAY = parseInt(getMetaValue('delay'), 10);
const AUTO_TAILING_OFFSET = parseInt(getMetaValue('auto_tailing_offset'), 10);
const ANIMATION_SPEED = parseInt(getMetaValue('animation_speed'), 10);
const TOTAL_ATTEMPTS = parseInt(getMetaValue('total_attempts'), 10);

function recurse(delay = DELAY) {
return new Promise((resolve) => setTimeout(resolve, delay));
}

// Enable auto tailing only when users scroll down to the bottom
// of the page. This prevent auto tailing the page if users want
// to view earlier rendered messages.
function checkAutoTailingCondition() {
const docHeight = $(document).height();
console.debug($(window).scrollTop());
console.debug($(window).height());
console.debug($(document).height());
return $(window).scrollTop() !== 0
&& ($(window).scrollTop() + $(window).height() > docHeight - AUTO_TAILING_OFFSET);
}

function toggleWrap() {
$('pre code').toggleClass('wrap');
}

function scrollBottom() {
$('html, body').animate({ scrollTop: $(document).height() }, ANIMATION_SPEED);
}

window.toggleWrapLogs = toggleWrap;
window.scrollBottomLogs = scrollBottom;

// Streaming log with auto-tailing.
function autoTailingLog(tryNumber, metadata = null, autoTailing = false) {
console.debug(`Auto-tailing log for dag_id: ${dagId}, task_id: ${taskId}, \
execution_date: ${executionDate}, try_number: ${tryNumber}, metadata: ${JSON.stringify(metadata)}`);

return Promise.resolve(
$.ajax({
url: logsWithMetadataUrl,
data: {
dag_id: dagId,
task_id: taskId,
execution_date: executionDate,
try_number: tryNumber,
metadata: JSON.stringify(metadata),
},
}),
).then((res) => {
// Stop recursive call to backend when error occurs.
if (!res) {
document.getElementById(`loading-${tryNumber}`).style.display = 'none';
return;
}
// res.error is a boolean
// res.message is the log itself or the error message
if (res.error) {
if (res.message) {
console.error(`Error while retrieving log: ${res.message}`);
}
document.getElementById(`loading-${tryNumber}`).style.display = 'none';
return;
}

if (res.message) {
// Auto scroll window to the end if current window location is near the end.
let shouldScroll = false;
if (autoTailing && checkAutoTailingCondition()) {
shouldScroll = true;
}

// Detect urls
const urlRegex = /http(s)?:\/\/[\w\.\-]+(\.?:[\w\.\-]+)*([\/?#][\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=\.%]+)?/g;

res.message.forEach((item) => {
const logBlockElementId = `try-${tryNumber}-${item[0]}`;
let logBlock = document.getElementById(logBlockElementId);
if (!logBlock) {
const logDivBlock = document.createElement('div');
const logPreBlock = document.createElement('pre');
logDivBlock.appendChild(logPreBlock);
logPreBlock.innerHTML = `<code id="${logBlockElementId}" ></code>`;
document.getElementById(`log-group-${tryNumber}`).appendChild(logDivBlock);
logBlock = document.getElementById(logBlockElementId);
}

// The message may contain HTML, so either have to escape it or write it as text.
const escapedMessage = escapeHtml(item[1]);
const linkifiedMessage = escapedMessage.replace(urlRegex, (url) => `<a href="${url}" target="_blank">${url}</a>`);
logBlock.innerHTML += `${linkifiedMessage}\n`;
});

// Auto scroll window to the end if current window location is near the end.
if (shouldScroll) {
scrollBottom();
}
}

if (res.metadata.end_of_log) {
document.getElementById(`loading-${tryNumber}`).style.display = 'none';
return;
}
recurse().then(() => autoTailingLog(
tryNumber, res.metadata, autoTailing,
));
});
}
$(document).ready(() => {
// Lazily load all past task instance logs.
// TODO: We only need to have recursive queries for
// latest running task instances. Currently it does not
// work well with ElasticSearch because ES query only
// returns at most 10k documents. We want the ability
// to display all logs in the front-end.
// An optimization here is to render from latest attempt.
for (let i = TOTAL_ATTEMPTS; i >= 1; i -= 1) {
// Only autoTailing the page when streaming the latest attempt.
const autoTailing = i === TOTAL_ATTEMPTS;
autoTailingLog(i, null, autoTailing);
}
});
144 changes: 19 additions & 125 deletions airflow/www/templates/airflow/ti_log.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
{% extends "airflow/task_instance.html" %}
{% block title %}DAGs - {{ appbuilder.app_name }}{% endblock %}

{% block head_meta %}
{{ super() }}
<meta name="dag_id" content="{{ dag_id }}">
<meta name="task_id" content="{{ task_id }}">
<meta name="execution_date" content="{{ execution_date }}">
<meta name="logs_with_metadata_url" content="{{ url_for('Airflow.get_logs_with_metadata') }}">
{# Time interval to wait before next log fetching. Default 2s. #}
<meta name="delay" content="{{ (log_fetch_delay_sec | int ) * 1000 }}">
{# Distance away from page bottom to enable auto tailing. #}
<meta name="auto_tailing_offset" content="{{ log_auto_tailing_offset | int }}">
{# Animation speed for auto tailing log display. #}
<meta name="animation_speed" content="{{ log_animation_speed | int }}">
{# Total number of tabs to show. #}
<meta name="total_attempts" content="{{ logs|length }}">
{% endblock %}

{% block content %}
{{ super() }}
<h4>{{ title }}</h4>
Expand All @@ -36,8 +52,8 @@ <h4>{{ title }}</h4>
</ul>
</div>
<div class="col-md-4 text-right">
<a class="btn btn-default" onclick="scrollBottom()">Jump To End</a>
<a class="btn btn-default" onclick="toggleWrap()">Toggle Wrap</a>
<a class="btn btn-default" onclick="scrollBottomLogs()">Jump To End</a>
<a class="btn btn-default" onclick="toggleWrapLogs()">Toggle Wrap</a>
</div>
</div>
<div class="tab-content">
Expand All @@ -53,127 +69,5 @@ <h4>{{ title }}</h4>
{% endblock %}
{% block tail %}
{{ super() }}
<script>
// Time interval to wait before next log fetching. Default 2s.
const DELAY = "{{ (log_fetch_delay_sec | int ) * 1000 }}";
// Distance away from page bottom to enable auto tailing.
const AUTO_TAILING_OFFSET = "{{ log_auto_tailing_offset | int }}";
// Animation speed for auto tailing log display.
const ANIMATION_SPEED = "{{ log_animation_speed | int }}";
// Total number of tabs to show.
const TOTAL_ATTEMPTS = "{{ logs|length }}";

// Recursively fetch logs from flask endpoint.
function recurse(delay=DELAY) {
return new Promise((resolve) => setTimeout(resolve, delay));
}

// Enable auto tailing only when users scroll down to the bottom
// of the page. This prevent auto tailing the page if users want
// to view earlier rendered messages.
function checkAutoTailingCondition() {
const docHeight = $(document).height();
console.debug($(window).scrollTop())
console.debug($(window).height())
console.debug($(document).height())
return $(window).scrollTop() != 0
&& ($(window).scrollTop() + $(window).height() > docHeight - AUTO_TAILING_OFFSET);
}

function toggleWrap() {
$("pre code").toggleClass("wrap")
}

function scrollBottom() {
$("html, body").animate({ scrollTop: $(document).height() }, ANIMATION_SPEED);
}

// Streaming log with auto-tailing.
function autoTailingLog(try_number, metadata=null, auto_tailing=false) {
console.debug("Auto-tailing log for dag_id: {{ dag_id }}, task_id: {{ task_id }}, \
execution_date: {{ execution_date }}, try_number: " + try_number + ", metadata: " + JSON.stringify(metadata));

return Promise.resolve(
$.ajax({
url: "{{ url_for("Airflow.get_logs_with_metadata") }}",
data: {
dag_id: "{{ dag_id }}",
task_id: "{{ task_id }}",
execution_date: "{{ execution_date }}",
try_number: try_number,
metadata: JSON.stringify(metadata),
},
})).then(res => {
// Stop recursive call to backend when error occurs.
if (!res) {
document.getElementById("loading-"+try_number).style.display = "none";
return;
}
// res.error is a boolean
// res.message is the log itself or the error message
if (res.error) {
if (res.message) {
console.error("Error while retrieving log: " + res.message);
}
document.getElementById("loading-"+try_number).style.display = "none";
return;
}

if (res.message) {
// Auto scroll window to the end if current window location is near the end.
if(auto_tailing && checkAutoTailingCondition()) {
var should_scroll = true;
}

// Detect urls
var url_regex = /http(s)?:\/\/[\w\.\-]+(\.?:[\w\.\-]+)*([\/?#][\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=\.%]+)?/g;

res.message.forEach(function(item, index){
var log_block_element_id = "try-" + try_number + "-" + item[0];
var log_block = document.getElementById(log_block_element_id);
if (!log_block) {
log_div_block = document.createElement('div');
log_pre_block = document.createElement('pre');
log_div_block.appendChild(log_pre_block);
log_pre_block.innerHTML = "<code id=\"" + log_block_element_id + "\" ></code>";
document.getElementById("log-group-" + try_number).appendChild(log_div_block);
log_block = document.getElementById(log_block_element_id);
}

// The message may contain HTML, so either have to escape it or write it as text.
var escaped_message = escapeHtml(item[1]);
var linkified_message = escaped_message.replace(url_regex, function(url) {
return "<a href=\"" + url + "\" target=\"_blank\">" + url + "</a>";
});
log_block.innerHTML += linkified_message + "\n";
})

// Auto scroll window to the end if current window location is near the end.
if(should_scroll) {
scrollBottom();
}
}

if (res.metadata.end_of_log) {
document.getElementById("loading-"+try_number).style.display = "none";
return;
}
return recurse().then(() => autoTailingLog(
try_number, res.metadata, auto_tailing));
});
}
$(document).ready(function() {
// Lazily load all past task instance logs.
// TODO: We only need to have recursive queries for
// latest running task instances. Currently it does not
// work well with ElasticSearch because ES query only
// returns at most 10k documents. We want the ability
// to display all logs in the front-end.
// An optimization here is to render from latest attempt.
for(let i = TOTAL_ATTEMPTS; i >= 1; i--) {
// Only auto_tailing the page when streaming the latest attempt.
autoTailingLog(i, null, auto_tailing=(i == TOTAL_ATTEMPTS));
}
});
</script>
<script src="{{ url_for_asset('tiLog.js') }}"></script>
{% endblock %}
1 change: 1 addition & 0 deletions airflow/www/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const config = {
switch: `${CSS_DIR}/switch.css`,
taskInstances: `${JS_DIR}/task_instances.js`,
taskInstance: `${JS_DIR}/task_instance.js`,
tiLog: `${JS_DIR}/ti_log.js`,
tree: [`${CSS_DIR}/tree.css`, `${JS_DIR}/tree.js`],
circles: `${JS_DIR}/circles.js`,
durationChart: `${JS_DIR}/duration_chart.js`,
Expand Down

0 comments on commit 9dd14aa

Please sign in to comment.