Skip to content
Closed
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
110 changes: 110 additions & 0 deletions front/src/views/AppLogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
class="flex-grow-1"
/>
<v-btn @click="get" :disabled="disabled" color="primary" height="40">Show logs</v-btn>
<v-btn @click="toggleLive" :color="live ? 'green' : ''" outlined height="40" :disabled="!configured || query.view !== 'messages'">
<v-icon small class="mr-1">{{ live ? 'mdi-pause' : 'mdi-play' }}</v-icon>
{{ live ? 'Live: ON' : 'Live: OFF' }}
</v-btn>
</div>

<v-btn-toggle v-model="query.view" @change="setQuery" mandatory dense class="mt-2">
Expand Down Expand Up @@ -149,6 +153,8 @@ export default {
loading: false,
loadingError: '',
data: {},
live: false,
liveInterval: null,
query: {
source: q.source || 'agent',
view: q.view || 'messages',
Expand Down Expand Up @@ -306,6 +312,9 @@ export default {
this.data.chart = null;
this.data.entries = null;
this.data.patterns = null;
if (this.live) {
this.stopLive();
}
this.$api.getLogs(this.appId, this.$route.query.query, (data, error) => {
this.loading = false;
const errMsg = 'Failed to load logs';
Expand All @@ -320,6 +329,9 @@ export default {
this.saved = JSON.stringify(this.form);
this.query.source = this.data.source;
this.query.view = this.data.view;
if (this.query.view === 'messages' && this.live && this.configured) {
this.startLive();
}
});
},
save() {
Expand All @@ -340,6 +352,104 @@ export default {
this.get();
});
},
buildLogsQuery() {
return JSON.stringify({
source: this.query.source,
filters: this.query.filters,
limit: this.query.limit,
view: 'messages',
});
},
startLive() {
if (this.liveInterval || !this.configured) return;

this.live = true;

let lastTimestamp = Date.now() * 1000; // Convert to microseconds for ClickHouse

const pollLogs = () => {
if (!this.live) return;

const now = Date.now() * 1000; // Current time in microseconds

// Adjust time range: from last timestamp + 1μs to now
const timeQuery = {
from: Math.floor(lastTimestamp / 1000) + 1, // Convert back to milliseconds and add 1ms
to: Math.floor(now / 1000),
};

const queryParams = new URLSearchParams({
...this.$route.query,
...timeQuery,
query: this.buildLogsQuery(),
});

this.$api.getLogs(this.appId, queryParams.toString(), (data, error) => {
if (error) {
return;
}

if (data.status === 'warning') {
return;
}

// Process new entries
if (data.entries && data.entries.length > 0) {
const newEntries = data.entries.map((e) => {
const newline = e.message ? e.message.indexOf('\n') : -1;
return {
...e,
color: palette.get(e.color),
date: this.$format.date(e.timestamp, '{MMM} {DD} {HH}:{mm}:{ss}'),
multiline: newline > 0 ? newline : 0,
};
});

if (!this.data.entries) {
this.$set(this.data, 'entries', []);
}

// Add new entries to the beginning (newest first)
this.data.entries.unshift(...newEntries.reverse());

// Update last timestamp to the newest entry
const timestamps = data.entries.map((e) => e.timestamp);
if (timestamps.length > 0) {
lastTimestamp = Math.max(...timestamps.map((t) => new Date(t).getTime() * 1000));
}

// Keep limit of entries on screen
if (this.data.entries.length > this.query.limit) {
this.data.entries.splice(this.query.limit);
}
} else {
// Update timestamp even if no new entries
lastTimestamp = now;
}
});
};

// Initial poll and set up interval for every 2 seconds
pollLogs();
this.liveInterval = setInterval(pollLogs, 2000);
},
stopLive() {
if (this.liveInterval) {
clearInterval(this.liveInterval);
this.liveInterval = null;
}
this.live = false;
},
toggleLive() {
if (this.live) {
this.stopLive();
} else {
if (this.query.view === 'messages' && this.configured) {
this.data.entries = [];
this.startLive();
}
}
},
},
};
</script>
Expand Down
134 changes: 133 additions & 1 deletion front/src/views/Logs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
class="flex-grow-1"
/>
<v-btn @click="get" :disabled="disabled" color="primary" height="40">Show logs</v-btn>
<v-btn @click="toggleLive" :color="live ? 'green' : ''" outlined height="40">
<v-icon small class="mr-1">{{ live ? 'mdi-pause' : 'mdi-play' }}</v-icon>
{{ live ? 'Live: ON' : 'Live: OFF' }}
</v-btn>
</div>
<div class="d-flex gap-2 sources">
<v-checkbox v-model="query.agent" label="Container logs" :disabled="disabled" dense hide-details />
Expand Down Expand Up @@ -117,6 +121,8 @@ export default {
loading: false,
error: '',
view: {},
live: false,
liveInterval: null,
query: {
view: q.view || 'messages',
agent: q.agent !== undefined ? q.agent : true,
Expand Down Expand Up @@ -157,7 +163,8 @@ export default {
if (!this.view.entries) {
return null;
}
return this.view.entries.map((e) => {
const sorted = [...this.view.entries].sort((a, b) => b.timestamp - a.timestamp);
return sorted.map((e) => {
const message = e.message.trim();
const newline = message.indexOf('\n');
let application = e.application;
Expand Down Expand Up @@ -239,15 +246,140 @@ export default {
this.loading = true;
this.error = '';
this.view.entries = null;
if (this.live) {
this.stopLive();
}
this.$api.getOverview('logs', JSON.stringify(this.query), (data, error) => {
this.loading = false;
if (error) {
this.error = error;
return;
}
this.view = data.logs || {};
if (this.query.view === 'messages' && this.live) {
this.startLive();
}
});
},
buildLogsQuery() {
return JSON.stringify({
agent: this.query.agent,
otel: this.query.otel,
filters: this.query.filters,
limit: this.query.limit,
view: 'messages',
});
},
startLive() {
if (this.liveInterval) return;

this.live = true;

let lastTimestamp = Date.now() * 1000; // Convert to microseconds for ClickHouse

const pollLogs = () => {
if (!this.live) return;

const now = Date.now() * 1000; // Current time in microseconds

// Adjust time range: from last timestamp + 1μs to now
const timeParams = {
from: Math.floor(lastTimestamp / 1000) + 1, // Convert back to milliseconds and add 1ms
to: Math.floor(now / 1000),
};

// Create query with time range - same approach as AppLogs.vue
const liveQuery = {
...this.query,
from: timeParams.from,
to: timeParams.to,
};

// Use $api.getOverview like the regular get() method, but with time range
this.$api.getOverview('logs', JSON.stringify(liveQuery), (data, error) => {
if (error) {
return;
}

if (data.status === 'warning') {
return;
}

// Process new entries - check both possible structures
const entries = data.logs?.entries || data.entries;
if (entries && entries.length > 0) {
const newEntries = entries.map((e) => {
const message = (e.message || '').trim();
const newline = message.indexOf('\n');
let application = e.application;
let link;

if (application && application.includes(':')) {
const id = this.$utils.appId(application);
application = id.name;
link = {
name: 'overview',
params: { view: 'applications', id: e.application, report: 'Logs' },
query: this.$utils.contextQuery(),
};
}

return {
...e,
application,
link,
message,
color: palette.get(e.color),
date: this.$format.date(e.timestamp, '{MMM} {DD} {HH}:{mm}:{ss}'),
multiline: newline > 0 ? newline : 0,
};
});

if (!this.view.entries) {
this.$set(this.view, 'entries', []);
}

// Add new entries to the end (chronological order)
this.view.entries.push(...newEntries);

// Update last timestamp to the newest entry
const timestamps = entries.map((e) => new Date(e.timestamp).getTime() * 1000);
if (timestamps.length > 0) {
lastTimestamp = Math.max(...timestamps);
}

// Keep limit of entries on screen - remove oldest entries
if (this.view.entries.length > this.query.limit) {
this.view.entries.splice(0, this.view.entries.length - this.query.limit);
}
} else {
// Update timestamp even if no new entries
lastTimestamp = now;
}
});
};

// Initial poll and set up interval for every 2 seconds
pollLogs();
this.liveInterval = setInterval(pollLogs, 2000);
},
stopLive() {
if (this.liveInterval) {
clearInterval(this.liveInterval);
this.liveInterval = null;
}
this.live = false;
},
toggleLive() {
if (this.live) {
this.stopLive();
} else {
if (this.query.view === 'messages') {
this.view.entries = [];
}
this.startLive();
}
},
zoom(s) {
const { from, to } = s.selection;
const query = { ...this.$route.query, from, to };
Expand Down