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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ spec:
| alertingRuleTenantLabelKey | name of the alerting rule label used to match the tenantId for log-based alerts. Allows log-based alerts to request metrics to the proper tenant endpoint | `tenantId` | string |
| alertingRuleNamespaceLabelKey | name of the label used to filter alerting rules by namespace | `kubernetes_namespace_name` | string |
| useTenantInHeader | whether or not the tenant header `X-Scope-OrgID` should be used instead of using the tenant in the URL request | `false` | boolean |
| showTimezoneSelector | whether or not to show the timezone selector in the UI | `false` | boolean |

## Build a testint the image

Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type PluginConfig struct {
Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
LogsLimit int `json:"logsLimit,omitempty" yaml:"logsLimit,omitempty"`
Schema string `json:"schema,omitempty" yaml:"schema,omitempty"`
ShowTimezoneSelector bool `json:"showTimezoneSelector,omitempty" yaml:"showTimezoneSelector,omitempty"`
}

func (pluginConfig *PluginConfig) MarshalJSON() ([]byte, error) {
Expand Down
7 changes: 6 additions & 1 deletion web/locales/en/plugin__logging-view-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"Explain Log Volume": "Explain Log Volume",
"Search by {{attributeName}}": "Search by {{attributeName}}",
"Attribute": "Attribute",
"No results found for": "No results found for",
"Filter by {{attributeName}}": "Filter by {{attributeName}}",
"Search": "Search",
"Loading...": "Loading...",
Expand Down Expand Up @@ -131,6 +132,10 @@
"From": "From",
"To": "To",
"Invalid date range": "Invalid date range",
"Select timezone": "Select timezone",
"Clear input value": "Clear input value",
"All Timezones": "All Timezones",
"Common Timezones": "Common Timezones",
"Hide Histogram": "Hide Histogram",
"Show Histogram": "Show Histogram",
"Pause streaming": "Pause streaming",
Expand All @@ -141,4 +146,4 @@
"Aggregated Logs": "Aggregated Logs",
"Logs": "Logs",
"Please select a namespace": "Please select a namespace"
}
}
163 changes: 159 additions & 4 deletions web/src/__tests__/date-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { DateFormat, dateToFormat } from '../date-utils';
import {
DateFormat,
dateToFormat,
getBrowserTimezone,
getTimezoneOffset,
normalizeTimezone,
parseInTimezone,
} from '../date-utils';

const mockDate = new Date('2023-02-22T08:38:36Z');
const mockDateWithMillis = new Date('2023-02-22T08:38:36.001Z');
Expand All @@ -14,7 +21,7 @@ describe('date utils', () => {
{ format: DateFormat.TimeMed, output: '08:38:36' },
{ format: DateFormat.TimeShort, output: '08:38' },
].forEach(({ format, output }) => {
expect(dateToFormat(mockDate, format)).toEqual(output);
expect(dateToFormat(mockDate, format, 'UTC')).toEqual(output);
});
});

Expand All @@ -26,7 +33,7 @@ describe('date utils', () => {
{ format: DateFormat.TimeMed, output: '08:38:36' },
{ format: DateFormat.TimeShort, output: '08:38' },
].forEach(({ format, output }) => {
expect(dateToFormat(mockDateWithMillis, format)).toEqual(output);
expect(dateToFormat(mockDateWithMillis, format, 'UTC')).toEqual(output);
});
});

Expand All @@ -38,11 +45,159 @@ describe('date utils', () => {
{ format: DateFormat.TimeMed, output: '08:08:06' },
{ format: DateFormat.TimeShort, output: '08:08' },
].forEach(({ format, output }) => {
expect(dateToFormat(mockDateWithValuesUnderTen, format)).toEqual(output);
expect(dateToFormat(mockDateWithValuesUnderTen, format, 'UTC')).toEqual(output);
});
});

it('should return invalid date when the date is invalid', () => {
expect(dateToFormat(invalidDate, DateFormat.DateMed)).toEqual('invalid date');
});

it('should format a date in a specific timezone', () => {
// 08:38:36 UTC = 03:38:36 in America/New_York (EST, UTC-5)
const utcDate = new Date('2023-02-22T08:38:36Z');
expect(dateToFormat(utcDate, DateFormat.TimeMed, 'America/New_York')).toEqual('03:38:36');
expect(dateToFormat(utcDate, DateFormat.TimeMed, 'UTC')).toEqual('08:38:36');

// 08:38:36 UTC = 14:08:36 in Asia/Kolkata (IST, UTC+5:30)
expect(dateToFormat(utcDate, DateFormat.TimeMed, 'Asia/Kolkata')).toEqual('14:08:36');
});

it('should format DateMed in a specific timezone', () => {
// 2023-02-22T02:00:00 UTC = 2023-02-21 21:00 in America/New_York
const utcDate = new Date('2023-02-22T02:00:00Z');
expect(dateToFormat(utcDate, DateFormat.DateMed, 'America/New_York')).toEqual('2023-02-21');
expect(dateToFormat(utcDate, DateFormat.DateMed, 'UTC')).toEqual('2023-02-22');
});
});

describe('normalizeTimezone', () => {
it('should return undefined for empty string', () => {
expect(normalizeTimezone('')).toBeUndefined();
});

it('should return undefined for whitespace-only string', () => {
expect(normalizeTimezone(' ')).toBeUndefined();
});

it('should return undefined for undefined input', () => {
expect(normalizeTimezone(undefined)).toBeUndefined();
});

it('should return the timezone for valid string', () => {
expect(normalizeTimezone('UTC')).toEqual('UTC');
expect(normalizeTimezone('America/New_York')).toEqual('America/New_York');
});

it('should trim whitespace from valid timezone', () => {
expect(normalizeTimezone(' UTC ')).toEqual('UTC');
});
});

describe('getBrowserTimezone', () => {
it('should return a non-empty string', () => {
const tz = getBrowserTimezone();
expect(typeof tz).toBe('string');
expect(tz.length).toBeGreaterThan(0);
});

it('should return a valid IANA timezone identifier', () => {
const tz = getBrowserTimezone();
// Valid IANA timezones contain a slash (e.g., "America/New_York") or are "UTC"
expect(tz === 'UTC' || tz.includes('/')).toBe(true);
});
});

describe('getTimezoneOffset', () => {
const testDate = new Date('2023-02-22T12:00:00Z');

it('should return offset for UTC', () => {
const result = getTimezoneOffset('UTC', testDate);
expect(result.offsetMinutes).toEqual(0);
expect(result.label).toMatch(/GMT|UTC/);
});

it('should return negative offset for America/New_York (EST)', () => {
const result = getTimezoneOffset('America/New_York', testDate);
// EST is UTC-5 = -300 minutes
expect(result.offsetMinutes).toEqual(-300);
expect(result.label).toMatch(/GMT-0?5(:00)?/);
});

it('should return positive offset for Asia/Kolkata', () => {
const result = getTimezoneOffset('Asia/Kolkata', testDate);
// IST is UTC+5:30 = +330 minutes
expect(result.offsetMinutes).toEqual(330);
expect(result.label).toMatch(/GMT\+0?5:30/);
});

it('should return positive offset for Europe/Paris (CET)', () => {
const result = getTimezoneOffset('Europe/Paris', testDate);
// CET is UTC+1 = +60 minutes (in February, no DST)
expect(result.offsetMinutes).toEqual(60);
});

it('should return empty result for empty timezone', () => {
const result = getTimezoneOffset('');
expect(result.label).toEqual('');
expect(result.offsetMinutes).toEqual(0);
});

it('should return empty result for invalid timezone', () => {
const result = getTimezoneOffset('Invalid/Timezone');
expect(result.label).toEqual('');
expect(result.offsetMinutes).toEqual(0);
});
});

describe('parseInTimezone', () => {
it('should return NaN for invalid date string', () => {
expect(parseInTimezone('invalid', '12:00:00', 'UTC')).toBeNaN();
expect(parseInTimezone('2023-02-22', 'invalid', 'UTC')).toBeNaN();
});

it('should parse date/time in UTC correctly', () => {
const result = parseInTimezone('2023-02-22', '12:00:00', 'UTC');
const expected = new Date('2023-02-22T12:00:00Z').getTime();
expect(result).toEqual(expected);
});

it('should parse date/time in America/New_York correctly', () => {
// 12:00 in New York (EST, UTC-5) = 17:00 UTC
const result = parseInTimezone('2023-02-22', '12:00:00', 'America/New_York');
const expected = new Date('2023-02-22T17:00:00Z').getTime();
expect(result).toEqual(expected);
});

it('should parse date/time in Asia/Kolkata correctly', () => {
// 12:00 in Kolkata (IST, UTC+5:30) = 06:30 UTC
const result = parseInTimezone('2023-02-22', '12:00:00', 'Asia/Kolkata');
const expected = new Date('2023-02-22T06:30:00Z').getTime();
expect(result).toEqual(expected);
});

it('should handle milliseconds in time string', () => {
const result = parseInTimezone('2023-02-22', '12:00:00.500', 'UTC');
const expected = new Date('2023-02-22T12:00:00.500Z').getTime();
expect(result).toEqual(expected);
});

it('should use browser timezone when timezone is empty', () => {
const browserTz = getBrowserTimezone();
const resultEmpty = parseInTimezone('2023-02-22', '12:00:00', '');
const resultBrowser = parseInTimezone('2023-02-22', '12:00:00', browserTz);
expect(resultEmpty).toEqual(resultBrowser);
});

it('should handle date boundaries correctly', () => {
// 01:00 in New York (EST, UTC-5) = 06:00 UTC same day
const result1 = parseInTimezone('2023-02-22', '01:00:00', 'America/New_York');
const expected1 = new Date('2023-02-22T06:00:00Z').getTime();
expect(result1).toEqual(expected1);

// 23:00 in Kolkata (IST, UTC+5:30) = 17:30 UTC same day
const result2 = parseInTimezone('2023-02-22', '23:00:00', 'Asia/Kolkata');
const expected2 = new Date('2023-02-22T17:30:00Z').getTime();
expect(result2).toEqual(expected2);
});
});
2 changes: 1 addition & 1 deletion web/src/components/filters/search-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const SearchSelect: React.FC<SearchSelectProps> = ({
newSelectOptions = [
{
isAriaDisabled: true,
children: `No results found for "${inputValue}"`,
children: `${t('No results found for')} "${inputValue}"`,
value: NO_RESULTS,
hasCheckbox: false,
},
Expand Down
37 changes: 26 additions & 11 deletions web/src/components/logs-histogram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ interface LogHistogramProps {
isLoading?: boolean;
error?: unknown;
schema?: Schema;
timezone?: string;
}

const resultHasAbreviation = (
Expand Down Expand Up @@ -112,13 +113,14 @@ const metricValueToChartData = (
group: Severity,
value: Array<MetricValue>,
interval: number,
timezone?: string,
): Array<ChartData> => {
const timeEntries = new Set<number>();
const chartData: Array<ChartData> = [];

for (const metric of value) {
const time = parseFloat(String(metric[0])) * 1000;
const formattedTime = dateToFormat(time, getTimeFormatFromInterval(interval));
const formattedTime = dateToFormat(time, getTimeFormatFromInterval(interval), timezone);

// Prevent duplicate entries to avoid chart rendering issues
if (!timeEntries.has(time)) {
Expand All @@ -136,12 +138,21 @@ const metricValueToChartData = (
return chartData;
};

const getChartsData = (data: HistogramData, interval: number): HistogramChartData => {
const getChartsData = (
data: HistogramData,
interval: number,
timezone?: string,
): HistogramChartData => {
const chartsData: HistogramChartData = {} as HistogramChartData;

Object.keys(data).forEach((group) => {
const severityGroup = severityFromString(group) ?? 'other';
const chartData = metricValueToChartData(severityGroup, data[severityGroup], interval);
const chartData = metricValueToChartData(
severityGroup,
data[severityGroup],
interval,
timezone,
);

chartsData[severityGroup] = chartData;
});
Expand All @@ -152,10 +163,9 @@ const getChartsData = (data: HistogramData, interval: number): HistogramChartDat
const tickCountFromTimeRange = (timeRange: TimeRangeNumber, interval: number): number =>
Math.ceil((timeRange.end - timeRange.start) / interval);

const HistogramTooltip: React.FC<ChartLegendTooltipProps & { interval: number }> = ({
interval,
...props
}) => {
const HistogramTooltip: React.FC<
ChartLegendTooltipProps & { interval: number; timezone?: string }
> = ({ interval, timezone, ...props }) => {
const { x: xProps, y: yProps, center, height } = props;

if (!center) {
Expand All @@ -180,7 +190,7 @@ const HistogramTooltip: React.FC<ChartLegendTooltipProps & { interval: number }>
<>
<ChartLegendTooltip
{...fixedProps}
title={(datum) => dateToFormat(datum.x ?? 0, getTimeFormatFromInterval(interval))}
title={(datum) => dateToFormat(datum.x ?? 0, getTimeFormatFromInterval(interval), timezone)}
constrainToVisibleArea
/>
<line
Expand Down Expand Up @@ -249,6 +259,7 @@ export const LogsHistogram: React.FC<LogHistogramProps> = ({
onChangeTimeRange,
interval,
schema,
timezone,
}) => {
const { t } = useTranslation('plugin__logging-view-plugin');

Expand Down Expand Up @@ -281,7 +292,7 @@ export const LogsHistogram: React.FC<LogHistogramProps> = ({
}

const data = aggregateMetricsLogData(histogramData, schema);
const chartsData = getChartsData(data, intervalValue);
const chartsData = getChartsData(data, intervalValue, timezone);

const tickCount = tickCountFromTimeRange(timeRangeValue, intervalValue);

Expand Down Expand Up @@ -386,7 +397,11 @@ export const LogsHistogram: React.FC<LogHistogramProps> = ({
`${datum.y !== null ? datum.y : t('No data')}`
}
labelComponent={
<HistogramTooltip interval={intervalValue} legendData={legendData} />
<HistogramTooltip
interval={intervalValue}
timezone={timezone}
legendData={legendData}
/>
}
voronoiDimension="x"
voronoiPadding={0}
Expand All @@ -406,7 +421,7 @@ export const LogsHistogram: React.FC<LogHistogramProps> = ({
tickCount={60}
fixLabelOverlap
tickFormat={(tick: number) =>
dateToFormat(tick, getTimeFormatFromTimeRange(timeRangeValue))
dateToFormat(tick, getTimeFormatFromTimeRange(timeRangeValue), timezone)
}
/>
<ChartAxis tickCount={2} dependentAxis tickFormat={valueWithScalePrefix} />
Expand Down
9 changes: 8 additions & 1 deletion web/src/components/logs-metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface LogsMetricsProps {
error?: unknown;
height?: number;
displayLegendTable?: boolean;
timezone?: string;
}

const GRAPH_HEIGHT = 250;
Expand Down Expand Up @@ -108,6 +109,7 @@ export const LogsMetrics: React.FC<LogsMetricsProps> = ({
timeRange,
height = GRAPH_HEIGHT,
displayLegendTable = false,
timezone,
}) => {
const { t } = useTranslation('plugin__logging-view-plugin');

Expand Down Expand Up @@ -184,7 +186,11 @@ export const LogsMetrics: React.FC<LogsMetricsProps> = ({
<ChartLegendTooltip
legendData={toolTipData}
title={(datum) =>
dateToFormat(datum.x ?? 0, getTimeFormatFromTimeRange(timeRangeValue))
dateToFormat(
datum.x ?? 0,
getTimeFormatFromTimeRange(timeRangeValue),
timezone,
)
}
/>
}
Expand Down Expand Up @@ -214,6 +220,7 @@ export const LogsMetrics: React.FC<LogsMetricsProps> = ({
dateToFormat(
tick,
intervalValue < 60 * 1000 ? DateFormat.TimeMed : DateFormat.TimeShort,
timezone,
)
}
/>
Expand Down
Loading