Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class ParseableClient {
}

/**
* Get logs for a specific dataset
* Get logs for a specific dataset using PostgreSQL syntax
*/
async getLogs(
baseUrl: string,
Expand All @@ -212,24 +212,61 @@ export class ParseableClient {
startTime = fiveMinutesAgo.toISOString();
}

// Build the SQL query
let sqlQuery = `select * from ${dataset}`;
// We'll use ISO format timestamps directly in the SQL query
// but keep the original ISO strings for the request body

// Build the SQL query with proper PostgreSQL syntax
let sqlQuery = `SELECT * FROM ${dataset}`;

// Add WHERE clause for search query if provided
const whereConditions = [];

// Add search query condition if provided
if (query && query.trim() !== '') {
sqlQuery += ` where ${query}`;
// If the query is a simple text search, wrap it in a LIKE clause
if (!query.includes('=') && !query.includes('<') && !query.includes('>') &&
!query.toLowerCase().includes(' and ') && !query.toLowerCase().includes(' or ')) {
// Search in all fields using ILIKE for case-insensitive search
whereConditions.push(`body ILIKE '%${query}%'`);
} else {
// User provided a more complex query, use it as is
whereConditions.push(query);
}
}

// Add time range conditions
if (startTime) {
whereConditions.push(`p_timestamp >= '${startTime}'`);
}

if (endTime) {
whereConditions.push(`p_timestamp <= '${endTime}'`);
}

// Combine all WHERE conditions
if (whereConditions.length > 0) {
sqlQuery += ` WHERE ${whereConditions.join(' AND ')}`;
}

// Add ORDER BY to get newest logs first
sqlQuery += ` ORDER BY p_timestamp DESC`;

// Add limit to the query if specified
if (limit && limit > 0) {
sqlQuery += ` limit ${limit}`;
sqlQuery += ` LIMIT ${limit}`;
}

const requestBody = {
query: sqlQuery,
startTime: startTime,
endTime: endTime,
streamName: dataset,
startTime: startTime || '',
endTime: endTime || ''
};

console.log('Request body:', JSON.stringify(requestBody, null, 2));

try {
console.log('Executing query:', sqlQuery);

const response = await this.fetchApi.fetch(`${baseUrl}/api/v1/query`, {
method: 'POST',
Expand All @@ -254,6 +291,44 @@ export class ParseableClient {
}
}

/**
* Get logs directly from the logstream API
* This is a simpler approach that doesn't use the query API
*/
async getLogsByStream(
baseUrl: string,
dataset: string,
limit: number = 100
): Promise<LogEntry[]> {
const headers = await this.getAuthHeader(baseUrl);

try {
// Use the logstream API endpoint directly
const url = `${baseUrl}/api/v1/logstream/${dataset}/logs?limit=${limit}`;

console.log('Fetching logs from logstream API:', url);

const response = await this.fetchApi.fetch(url, {
headers,
});

if (!response.ok) {
if (response.status === 401 || response.status === 403) {
throw new Error('Authentication failed. Please check your Parseable credentials.');
}
throw new Error(`Failed to fetch logs: ${response.statusText}`);
}

const data = await response.json();
return Array.isArray(data) ? data.map(entry => LogEntrySchema.parse(entry)) : [];
} catch (e) {
if (e instanceof z.ZodError) {
throw new Error(`Invalid log format from Parseable API: ${e.message}`);
}
throw e;
}
}

/**
* Export logs to CSV
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Grid } from '@material-ui/core';
import { LogStreamCard } from './LogStreamCard';

import React from 'react';
/**
* Component to render Parseable logstream content on an entity page
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
Expand Down Expand Up @@ -87,8 +87,22 @@ export const LogStreamCard = ({ title = 'Parseable Logs' }: LogStreamCardProps)

const [selectedDataset, setSelectedDataset] = useState<string>('');
const [logs, setLogs] = useState<LogEntry[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLiveTail, setIsLiveTail] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);

// Create a ref for the log container to handle auto-scrolling
const logContainerRef = useRef<HTMLDivElement>(null);

// Auto-scroll to bottom when logs update during live tail
useEffect(() => {
if (isLiveTail && logContainerRef.current) {
// Safely access scrollHeight with null check
const container = logContainerRef.current;
if (container) {
container.scrollTop = container.scrollHeight;
}
}
}, [logs, isLiveTail]);

const baseUrl = entity.metadata.annotations?.['parseable.io/base-url'] || '';

Expand All @@ -110,7 +124,7 @@ export const LogStreamCard = ({ title = 'Parseable Logs' }: LogStreamCardProps)
try {
const logData = await parseableClient.getLogs(baseUrl, selectedDataset);
setLogs(logData);
setError(undefined);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
setIsLiveTail(false);
Expand Down Expand Up @@ -159,12 +173,30 @@ export const LogStreamCard = ({ title = 'Parseable Logs' }: LogStreamCardProps)

// Render log content with proper formatting
const renderLogContent = (log: LogEntry) => {
// Convert log entry to string representation
const logString = JSON.stringify(log, null, 2);
// Convert log entry to string representation with safe handling of circular references
let logString = '';
try {
// Use a replacer function to handle circular references
const seen = new WeakSet();
logString = JSON.stringify(log, (_key, value) => {
// Handle objects to avoid circular references
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
return value;
}, 2);
} catch (error) {
// Fallback if JSON.stringify fails
const err = error instanceof Error ? error : new Error(String(error));
logString = `[Error stringifying log: ${err.message}]`;
}

return (
<div className={`${classes.logContent} ${getLogLevelColor(log)}`}>
{logString}
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{logString}</pre>
</div>
);
};
Expand Down Expand Up @@ -259,7 +291,7 @@ export const LogStreamCard = ({ title = 'Parseable Logs' }: LogStreamCardProps)
)}

{selectedDataset ? (
<Paper variant="outlined" className={classes.logContainer}>
<Paper ref={logContainerRef} variant="outlined" className={classes.logContainer}>
{logs.length === 0 ? (
<Typography variant="body2" align="center" style={{ padding: '16px' }}>
No logs found for the selected dataset
Expand Down
Loading