Skip to content

Commit

Permalink
feature: Log Context
Browse files Browse the repository at this point in the history
  • Loading branch information
SpencerTorres committed Sep 20, 2024
1 parent 4967d76 commit 5796334
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 29 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Features

- Implemented log context for log queries
- Added configuration options for log context columns

## 4.4.0

### Features
Expand Down
155 changes: 155 additions & 0 deletions src/components/LogsContextPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React from 'react';
import { Alert, Icon, IconName, useTheme2, VerticalGroup } from '@grafana/ui';
import { css } from '@emotion/css';
import { LogContextColumn } from 'data/CHDatasource';


const LogsContextPanelStyles = css`
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
width: 100%;
`;

interface LogContextPanelProps {
columns: LogContextColumn[];
datasourceUid: string;
}

const LogsContextPanel = (props: LogContextPanelProps) => {
const { columns, datasourceUid } = props;
const theme = useTheme2();

if (!columns || columns.length === 0) {
return (
<Alert title="" severity="warning">
<VerticalGroup>
<div>
{'Unable to match any context columns. Make sure your query returns at least one log context column from your '}
<a style={{ textDecoration: 'underline' }} href={`/connections/datasources/edit/${encodeURIComponent(datasourceUid)}#logs-config`}>ClickHouse Data Source settings</a>
</div>
</VerticalGroup>
</Alert>
);
}

return (
<div className={LogsContextPanelStyles}>
{columns.map((p) => (
<LogContextKey
key={p.name}
name={p.name}
value={p.value}
primaryColor={theme.colors.secondary.main}
primaryTextColor={theme.colors.text.primary}
secondaryColor={theme.colors.background.secondary}
secondaryTextColor={theme.colors.info.text}
/>
))}
</div>
)
};

/**
* Roughly match an icon with the context column name.
*/
const iconMatcher = (contextName: string): IconName => {
contextName = contextName.toLowerCase();

if (contextName === 'db' || contextName === 'database' || contextName.includes('data')) {
return 'database';
} else if (contextName.includes('service')) {
return 'building';
} else if (contextName.includes('error') || contextName.includes('warn') || contextName.includes('critical') || contextName.includes('fatal')) {
return 'exclamation-triangle';
} else if (contextName.includes('user') || contextName.includes('admin')) {
return 'user';
} else if (contextName.includes('email')) {
return 'at';
} else if (contextName.includes('file')) {
return 'file-alt';
} else if (contextName.includes('bug')) {
return 'bug';
} else if (contextName.includes('search')) {
return 'search';
} else if (contextName.includes('tag')) {
return 'tag-alt';
} else if (contextName.includes('span') || contextName.includes('stack')) {
return 'brackets-curly';
} if (contextName === 'host' || contextName === 'hostname' || contextName.includes('host')) {
return 'cloud';
} if (contextName === 'url' || contextName.includes('url')) {
return 'link';
} else if (contextName.includes('container') || contextName.includes('pod')) {
return 'cube';
}

return 'align-left';
}

interface LogContextKeyProps {
name: string;
value: string;
primaryColor: string;
primaryTextColor: string;
secondaryColor: string;
secondaryTextColor: string;
}

const LogContextKey = (props: LogContextKeyProps) => {
const { name, value, primaryColor, primaryTextColor, secondaryColor, secondaryTextColor } = props;

const styles = {
container: css`
display: flex;
justify-content: center;
align-items: center;
margin: 0.25em;
color: ${primaryTextColor}
`,
containerLeft: css`
display: flex;
align-items: center;
background-color: ${primaryColor};
border-radius: 2px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-top: 0.15em;
padding-bottom: 0.15em;
padding-left: 0.25em;
padding-right: 0.25em;
`,
contextName: css`
font-weight: bold;
padding-left: 0.25em;
user-select: all;
`,
contextValue: css`
background-color: ${secondaryColor};
color: ${secondaryTextColor};
border-radius: 2px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
user-select: all;
font-family: monospace;
padding-top: 0.15em;
padding-bottom: 0.15em;
padding-left: 0.25em;
padding-right: 0.25em;
`,
};

return (
<div className={styles.container}>
<div className={styles.containerLeft}>
<Icon name={iconMatcher(name)} size="md" />
<span className={styles.contextName}>{name}</span>
</div>
<span className={styles.contextValue}>{value}</span>
</div>
);
}

export default LogsContextPanel;
39 changes: 35 additions & 4 deletions src/components/configEditor/LogsConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import { ConfigSection, ConfigSubSection } from 'components/experimental/ConfigSection';
import { Input, Field } from '@grafana/ui';
import { Input, Field, InlineFormLabel, TagsInput } from '@grafana/ui';
import { OtelVersionSelect } from 'components/queryBuilder/OtelVersionSelect';
import { ColumnHint } from 'types/queryBuilder';
import otel, { defaultLogsTable } from 'otel';
import { LabeledInput } from './LabeledInput';
import { CHLogsConfig } from 'types/config';
import allLabels from 'labels';
import { columnLabelToPlaceholder } from 'data/utils';
import { Switch } from 'components/queryBuilder/Switch';

interface LogsConfigProps {
logsConfig?: CHLogsConfig;
Expand All @@ -18,18 +19,22 @@ interface LogsConfigProps {
onTimeColumnChange: (v: string) => void;
onLevelColumnChange: (v: string) => void;
onMessageColumnChange: (v: string) => void;
onSelectContextColumnsChange: (v: boolean) => void;
onContextColumnsChange: (v: string[]) => void;
}

export const LogsConfig = (props: LogsConfigProps) => {
const {
onDefaultDatabaseChange, onDefaultTableChange,
onOtelEnabledChange, onOtelVersionChange,
onTimeColumnChange, onLevelColumnChange, onMessageColumnChange
onTimeColumnChange, onLevelColumnChange, onMessageColumnChange,
onSelectContextColumnsChange, onContextColumnsChange
} = props;
let {
defaultDatabase, defaultTable,
otelEnabled, otelVersion,
timeColumn, levelColumn, messageColumn
timeColumn, levelColumn, messageColumn,
selectContextColumns, contextColumns
} = (props.logsConfig || {});
const labels = allLabels.components.Config.LogsConfig;

Expand All @@ -40,6 +45,8 @@ export const LogsConfig = (props: LogsConfigProps) => {
messageColumn = otelConfig.logColumnMap.get(ColumnHint.LogMessage);
}

const onContextColumnsChangeTrimmed = (columns: string[]) => onContextColumnsChange(columns.map(c => c.trim()).filter(c => c));

return (
<ConfigSection
title={labels.title}
Expand Down Expand Up @@ -74,7 +81,7 @@ export const LogsConfig = (props: LogsConfigProps) => {
placeholder={defaultLogsTable}
/>
</Field>
<ConfigSubSection
<ConfigSubSection
title={labels.columns.title}
description={labels.columns.description}
>
Expand Down Expand Up @@ -110,6 +117,30 @@ export const LogsConfig = (props: LogsConfigProps) => {
onChange={onMessageColumnChange}
/>
</ConfigSubSection>
<br/>
<ConfigSubSection
title={labels.contextColumns.title}
description={labels.contextColumns.description}
>
<Switch
label={labels.contextColumns.selectContextColumns.label}
tooltip={labels.contextColumns.selectContextColumns.tooltip}
value={selectContextColumns || false}
onChange={onSelectContextColumnsChange}
wide
/>
<div className="gf-form">
<InlineFormLabel width={12} className="query-keyword" tooltip={labels.contextColumns.columns.tooltip}>
{labels.contextColumns.columns.label}
</InlineFormLabel>
<TagsInput
placeholder={labels.contextColumns.columns.placeholder}
tags={contextColumns || []}
onChange={onContextColumnsChangeTrimmed}
width={60}
/>
</div>
</ConfigSubSection>
</ConfigSection>
);
}
5 changes: 3 additions & 2 deletions src/components/queryBuilder/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ interface SwitchProps {
label: string;
tooltip: string;
inline?: boolean;
wide?: boolean;
}

export const Switch = (props: SwitchProps) => {
const { value, onChange, label, tooltip, inline } = props;
const { value, onChange, label, tooltip, inline, wide } = props;

const theme = useTheme();
const switchContainerStyle: React.CSSProperties = {
Expand All @@ -25,7 +26,7 @@ export const Switch = (props: SwitchProps) => {

return (
<div className="gf-form">
<InlineFormLabel width={8} className={labelStyle} tooltip={tooltip}>
<InlineFormLabel width={wide ? 12 : 8} className={labelStyle} tooltip={tooltip}>
{label}
</InlineFormLabel>
<div style={switchContainerStyle}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/queryBuilder/views/LogsQueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const LogsQueryBuilder = (props: LogsQueryBuilderProps) => {
}, builderState);

useLogDefaultsOnMount(datasource, isNewQuery, builderOptions, builderOptionsDispatch);
useOtelColumns(builderState.otelEnabled, builderState.otelVersion, builderOptionsDispatch);
useOtelColumns(datasource, builderState.otelEnabled, builderState.otelVersion, builderOptionsDispatch);
useDefaultTimeColumn(datasource, allColumns, builderOptions.table, builderState.timeColumn, builderState.otelEnabled, builderOptionsDispatch);
useDefaultFilters(builderOptions.table, isNewQuery, builderOptionsDispatch);

Expand Down
34 changes: 32 additions & 2 deletions src/components/queryBuilder/views/logsQueryBuilderHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,25 @@ export const useLogDefaultsOnMount = (datasource: Datasource, isNewQuery: boolea
const defaultTable = datasource.getDefaultLogsTable() || datasource.getDefaultTable();
const otelVersion = datasource.getLogsOtelVersion();
const defaultColumns = datasource.getDefaultLogsColumns();
const shouldSelectLogContextColumns = datasource.shouldSelectLogContextColumns();
const contextColumnNames = datasource.getLogContextColumnNames();

const nextColumns: SelectedColumn[] = [];
const includedColumns = new Set<string>();
for (let [hint, colName] of defaultColumns) {
nextColumns.push({ name: colName, hint });
includedColumns.add(colName);
}

if (shouldSelectLogContextColumns) {
for (let columnName of contextColumnNames) {
if (includedColumns.has(columnName)) {
continue;
}

nextColumns.push({ name: columnName });
includedColumns.add(columnName);
}
}

builderOptionsDispatch(setOptions({
Expand All @@ -42,7 +57,7 @@ export const useLogDefaultsOnMount = (datasource: Datasource, isNewQuery: boolea
* Sets OTEL Logs columns automatically when OTEL is enabled.
* Does not run if OTEL is already enabled, only when it's changed.
*/
export const useOtelColumns = (otelEnabled: boolean, otelVersion: string, builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>) => {
export const useOtelColumns = (datasource: Datasource, otelEnabled: boolean, otelVersion: string, builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>) => {
const didSetColumns = useRef<boolean>(otelEnabled);
if (!otelEnabled) {
didSetColumns.current = false;
Expand All @@ -60,13 +75,28 @@ export const useOtelColumns = (otelEnabled: boolean, otelVersion: string, builde
}

const columns: SelectedColumn[] = [];
const includedColumns = new Set<string>();
logColumnMap.forEach((name, hint) => {
columns.push({ name, hint });
includedColumns.add(name);
});

const shouldSelectLogContextColumns = datasource.shouldSelectLogContextColumns();
const contextColumnNames = datasource.getLogContextColumnNames();
if (shouldSelectLogContextColumns) {
for (let columnName of contextColumnNames) {
if (includedColumns.has(columnName)) {
continue;
}

columns.push({ name: columnName });
includedColumns.add(columnName);
}
}

builderOptionsDispatch(setOptions({ columns }));
didSetColumns.current = true;
}, [otelEnabled, otelVersion, builderOptionsDispatch]);
}, [datasource, otelEnabled, otelVersion, builderOptionsDispatch]);
};

// Finds and selects a default log time column, updates when table changes
Expand Down
Loading

0 comments on commit 5796334

Please sign in to comment.