mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Trace UI demo (#20297)
* Add integration with Jeager Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split. Modifies build so that this branch docker images are pushed to docker hub Add a traceui dir with docker-compose and provision files for demoing.:wq * Enable docker logger plugin to send logs to loki * Add placeholder zipkin datasource * Fixed rebase issues, added enhanceDataFrame to non-legacy code path * Trace selector for jaeger query field * Fix logs default mode for Loki * Fix loading jaeger query field services on split * Updated grafana image in traceui/compose file * Fix prettier error * Hide behind feature flag, clean up unused code. * Fix tests * Fix tests * Cleanup code and review feedback * Remove traceui directory * Remove circle build changes * Fix feature toggles object * Fix merge issues * Fix some null errors * Fix test after strict null changes * Review feedback fixes * Fix toggle name Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
parent
b6f73e35a5
commit
ae09ccbf79
@ -26,7 +26,11 @@ export namespace dateMath {
|
||||
* @param roundUp See parseDateMath function.
|
||||
* @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used.
|
||||
*/
|
||||
export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: TimeZone): DateTime | undefined {
|
||||
export function parse(
|
||||
text?: string | DateTime | Date | null,
|
||||
roundUp?: boolean,
|
||||
timezone?: TimeZone
|
||||
): DateTime | undefined {
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ export interface DataLink {
|
||||
// 1: If exists, handle click directly
|
||||
// Not saved in JSON/DTO
|
||||
onClick?: (event: DataLinkClickEvent) => void;
|
||||
|
||||
// At the moment this is used for derived fields for metadata about internal linking.
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export type LinkTarget = '_blank' | '_self';
|
||||
|
@ -115,6 +115,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet
|
||||
logs?: boolean;
|
||||
annotations?: boolean;
|
||||
alerting?: boolean;
|
||||
tracing?: boolean;
|
||||
mixed?: boolean;
|
||||
hasQueryHelp?: boolean;
|
||||
category?: string;
|
||||
@ -316,6 +317,7 @@ export enum DataSourceStatus {
|
||||
export enum ExploreMode {
|
||||
Logs = 'Logs',
|
||||
Metrics = 'Metrics',
|
||||
Tracing = 'Tracing',
|
||||
}
|
||||
|
||||
export interface ExploreQueryFieldProps<
|
||||
|
@ -19,6 +19,7 @@ interface FeatureToggles {
|
||||
newEdit: boolean;
|
||||
meta: boolean;
|
||||
newVariables: boolean;
|
||||
tracingIntegration: boolean;
|
||||
}
|
||||
|
||||
interface LicenseInfo {
|
||||
@ -71,6 +72,7 @@ export class GrafanaBootConfig {
|
||||
newEdit: false,
|
||||
meta: false,
|
||||
newVariables: false,
|
||||
tracingIntegration: false,
|
||||
};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
phantomJSRenderer = false;
|
||||
|
@ -24,7 +24,7 @@ import { LogDetailsRow } from './LogDetailsRow';
|
||||
type FieldDef = {
|
||||
key: string;
|
||||
value: string;
|
||||
links?: string[];
|
||||
links?: Array<LinkModel<Field>>;
|
||||
fieldIndex?: number;
|
||||
};
|
||||
|
||||
@ -99,7 +99,7 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
return {
|
||||
key: field.name,
|
||||
value: field.values.get(row.rowIndex).toString(),
|
||||
links: links.map(link => link.href),
|
||||
links: links,
|
||||
fieldIndex: field.index,
|
||||
};
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { LogLabelStatsModel, GrafanaTheme } from '@grafana/data';
|
||||
import { Field, LinkModel, LogLabelStatsModel, GrafanaTheme } from '@grafana/data';
|
||||
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { withTheme } from '../../themes/index';
|
||||
@ -9,6 +9,7 @@ import { stylesFactory } from '../../themes/stylesFactory';
|
||||
|
||||
//Components
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { LinkButton } from '../Button/Button';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
parsedValue: string;
|
||||
@ -16,7 +17,7 @@ export interface Props extends Themeable {
|
||||
isLabel?: boolean;
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
links?: string[];
|
||||
links?: Array<LinkModel<Field>>;
|
||||
getStats: () => LogLabelStatsModel[] | null;
|
||||
}
|
||||
|
||||
@ -122,11 +123,27 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||
{links &&
|
||||
links.map(link => {
|
||||
return (
|
||||
<span key={link}>
|
||||
|
||||
<a href={link} target={'_blank'}>
|
||||
<i className={'fa fa-external-link'} />
|
||||
</a>
|
||||
<span key={link.href}>
|
||||
<>
|
||||
|
||||
<LinkButton
|
||||
variant={'transparent'}
|
||||
size={'sm'}
|
||||
icon={cx('fa', link.onClick ? 'fa-list' : 'fa-external-link')}
|
||||
href={link.href}
|
||||
target={'_blank'}
|
||||
onClick={
|
||||
link.onClick
|
||||
? event => {
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) {
|
||||
event.preventDefault();
|
||||
link.onClick(event);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
@ -92,7 +92,11 @@ func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||
_, err := sec.NewKey("path", "testdata/test-app")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
pm := &PluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
},
|
||||
}
|
||||
err = pm.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -18,7 +18,11 @@ func TestPluginDashboards(t *testing.T) {
|
||||
_, err := sec.NewKey("path", "testdata/test-app")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
pm := &PluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
},
|
||||
}
|
||||
err = pm.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -23,6 +23,7 @@ type DataSourcePlugin struct {
|
||||
Explore bool `json:"explore"`
|
||||
Table bool `json:"tables"`
|
||||
Logs bool `json:"logs"`
|
||||
Tracing bool `json:"tracing"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
|
@ -42,10 +42,12 @@ type PluginScanner struct {
|
||||
pluginPath string
|
||||
errors []error
|
||||
backendPluginManager backendplugin.Manager
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
@ -164,6 +166,7 @@ func (pm *PluginManager) scan(pluginDir string) error {
|
||||
scanner := &PluginScanner{
|
||||
pluginPath: pluginDir,
|
||||
backendPluginManager: pm.BackendPluginManager,
|
||||
cfg: pm.Cfg,
|
||||
}
|
||||
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
@ -213,6 +216,14 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
|
||||
return nil
|
||||
}
|
||||
|
||||
if !scanner.cfg.FeatureToggles["tracingIntegration"] {
|
||||
// Do not load tracing datasources if
|
||||
prefix := path.Join(setting.StaticRootPath, "app/plugins/datasource")
|
||||
if strings.Contains(currentPath, path.Join(prefix, "jaeger")) || strings.Contains(currentPath, path.Join(prefix, "zipkin")) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if f.Name() == "plugin.json" {
|
||||
err := scanner.loadPluginJson(currentPath)
|
||||
if err != nil {
|
||||
|
@ -15,7 +15,11 @@ func TestPluginScans(t *testing.T) {
|
||||
setting.StaticRootPath, _ = filepath.Abs("../../public/")
|
||||
setting.Raw = ini.Empty()
|
||||
|
||||
pm := &PluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
},
|
||||
}
|
||||
err := pm.Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
@ -34,7 +38,11 @@ func TestPluginScans(t *testing.T) {
|
||||
_, err = sec.NewKey("path", "testdata/test-app")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
pm := &PluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
},
|
||||
}
|
||||
err = pm.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -281,6 +281,7 @@ type Cfg struct {
|
||||
|
||||
ApiKeyMaxSecondsToLive int64
|
||||
|
||||
// Use to enable new features which may still be in alpha/beta stage.
|
||||
FeatureToggles map[string]bool
|
||||
}
|
||||
|
||||
|
@ -66,17 +66,17 @@ export interface GetExploreUrlArguments {
|
||||
datasourceSrv: DataSourceSrv;
|
||||
timeSrv: TimeSrv;
|
||||
}
|
||||
export async function getExploreUrl(args: GetExploreUrlArguments) {
|
||||
export async function getExploreUrl(args: GetExploreUrlArguments): Promise<string | undefined> {
|
||||
const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args;
|
||||
let exploreDatasource = panelDatasource;
|
||||
let exploreTargets: DataQuery[] = panelTargets;
|
||||
let url: string;
|
||||
let url: string | undefined;
|
||||
|
||||
// Mixed datasources need to choose only one datasource
|
||||
if (panelDatasource.meta.id === 'mixed' && exploreTargets) {
|
||||
if (panelDatasource.meta?.id === 'mixed' && exploreTargets) {
|
||||
// Find first explore datasource among targets
|
||||
for (const t of exploreTargets) {
|
||||
const datasource = await datasourceSrv.get(t.datasource);
|
||||
const datasource = await datasourceSrv.get(t.datasource || undefined);
|
||||
if (datasource) {
|
||||
exploreDatasource = datasource;
|
||||
exploreTargets = panelTargets.filter(t => t.datasource === datasource.name);
|
||||
@ -183,7 +183,7 @@ enum ParseUiStateIndex {
|
||||
Strategy = 3,
|
||||
}
|
||||
|
||||
export const safeParseJson = (text: string) => {
|
||||
export const safeParseJson = (text?: string): any | undefined => {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
@ -365,7 +365,7 @@ export function clearHistory(datasourceId: string) {
|
||||
}
|
||||
|
||||
export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
|
||||
const queryKeys = queries.reduce((newQueryKeys, query, index) => {
|
||||
const queryKeys = queries.reduce<string[]>((newQueryKeys, query, index) => {
|
||||
const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
|
||||
return newQueryKeys.concat(`${primaryKey}-${index}`);
|
||||
}, []);
|
||||
@ -381,7 +381,7 @@ export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRa
|
||||
};
|
||||
};
|
||||
|
||||
const parseRawTime = (value: any): TimeFragment => {
|
||||
const parseRawTime = (value: any): TimeFragment | null => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
@ -442,7 +442,7 @@ export const getValueWithRefId = (value?: any): any => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]) => {
|
||||
export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]): DataQueryError | undefined => {
|
||||
if (!errors) {
|
||||
return undefined;
|
||||
}
|
||||
@ -530,7 +530,7 @@ export const stopQueryState = (querySubscription: Unsubscribable) => {
|
||||
}
|
||||
};
|
||||
|
||||
export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues {
|
||||
export function getIntervals(range: TimeRange, lowLimit: string, resolution?: number): IntervalValues {
|
||||
if (!resolution) {
|
||||
return { interval: '1s', intervalMs: 1000 };
|
||||
}
|
||||
@ -542,7 +542,7 @@ export function deduplicateLogRowsById(rows: LogRowModel[]) {
|
||||
return _.uniqBy(rows, 'uid');
|
||||
}
|
||||
|
||||
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => {
|
||||
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]): DataQueryError | undefined => {
|
||||
const refId = getValueWithRefId(queryErrors);
|
||||
return refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
|
||||
return refId ? undefined : getFirstQueryErrorWithoutRefId(queryErrors);
|
||||
};
|
||||
|
@ -88,7 +88,7 @@ export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
|
||||
return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data);
|
||||
};
|
||||
|
||||
function serializeParams(data: Record<string, any>): string {
|
||||
export function serializeParams(data: Record<string, any>): string {
|
||||
return Object.keys(data)
|
||||
.map(key => {
|
||||
const value = data[key];
|
||||
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { DataQuery } from '@grafana/data';
|
||||
|
||||
export const getNextRefIdChar = (queries: DataQuery[]): string => {
|
||||
export const getNextRefIdChar = (queries: DataQuery[]): string | undefined => {
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return _.find(letters, refId => {
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { DataSourcePluginMeta, PluginType } from '@grafana/data';
|
||||
import { DataSourcePluginCategory } from 'app/types';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export function buildCategories(plugins: DataSourcePluginMeta[]): DataSourcePluginCategory[] {
|
||||
const categories: DataSourcePluginCategory[] = [
|
||||
{ id: 'tsdb', title: 'Time series databases', plugins: [] },
|
||||
{ id: 'logging', title: 'Logging & document databases', plugins: [] },
|
||||
config.featureToggles.tracingIntegration ? { id: 'tracing', title: 'Distributed tracing', plugins: [] } : null,
|
||||
{ id: 'sql', title: 'SQL', plugins: [] },
|
||||
{ id: 'cloud', title: 'Cloud', plugins: [] },
|
||||
{ id: 'enterprise', title: 'Enterprise plugins', plugins: [] },
|
||||
{ id: 'other', title: 'Others', plugins: [] },
|
||||
];
|
||||
].filter(item => item);
|
||||
|
||||
const categoryIndex: Record<string, DataSourcePluginCategory> = {};
|
||||
const pluginIndex: Record<string, DataSourcePluginMeta> = {};
|
||||
@ -66,6 +68,7 @@ function sortPlugins(plugins: DataSourcePluginMeta[]) {
|
||||
graphite: 95,
|
||||
loki: 90,
|
||||
mysql: 80,
|
||||
jaeger: 100,
|
||||
postgres: 79,
|
||||
gcloud: -1,
|
||||
};
|
||||
|
@ -148,13 +148,12 @@ describe('Explore', () => {
|
||||
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
|
||||
const queryErrors = setupErrors(true);
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
expect(queryError).toBeNull();
|
||||
expect(queryError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not filter out a generic error when looking for non-query-row-specific errors', async () => {
|
||||
const queryErrors = setupErrors();
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
expect(queryError).not.toBeNull();
|
||||
expect(queryError).toEqual({
|
||||
message: 'Error message',
|
||||
status: '400',
|
||||
|
@ -20,33 +20,33 @@ import {
|
||||
changeSize,
|
||||
initializeExplore,
|
||||
modifyQueries,
|
||||
refreshExplore,
|
||||
scanStart,
|
||||
setQueries,
|
||||
refreshExplore,
|
||||
updateTimeRange,
|
||||
toggleGraph,
|
||||
addQueryRow,
|
||||
updateTimeRange,
|
||||
} from './state/actions';
|
||||
// Types
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
GraphSeriesXY,
|
||||
PanelData,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
GraphSeriesXY,
|
||||
TimeZone,
|
||||
AbsoluteTimeRange,
|
||||
LoadingState,
|
||||
ExploreMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ExploreItemState, ExploreUrlState, ExploreId, ExploreUpdateState, ExploreUIState } from 'app/types/explore';
|
||||
import { ExploreId, ExploreItemState, ExploreUIState, ExploreUpdateState, ExploreUrlState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
import {
|
||||
ensureQueries,
|
||||
DEFAULT_RANGE,
|
||||
DEFAULT_UI_STATE,
|
||||
ensureQueries,
|
||||
getTimeRangeFromUrl,
|
||||
getTimeRange,
|
||||
lastUsedDatasourceKeyForOrgId,
|
||||
@ -70,6 +70,18 @@ const getStyles = stylesFactory(() => {
|
||||
button: css`
|
||||
margin: 1em 4px 0 0;
|
||||
`,
|
||||
// Utility class for iframe parents so that we can show iframe content with reasonable height instead of squished
|
||||
// or some random explicit height.
|
||||
fullHeight: css`
|
||||
label: fullHeight;
|
||||
height: 100%;
|
||||
`,
|
||||
iframe: css`
|
||||
label: iframe;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@ -328,14 +340,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
</button>
|
||||
</div>
|
||||
<ErrorContainer queryError={queryError} />
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
<AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`m-t-2 ${styles.logsMain}`} style={{ width }}>
|
||||
<main className={cx('m-t-2', styles.logsMain, styles.fullHeight)} style={{ width }}>
|
||||
<ErrorBoundaryAlert>
|
||||
{showStartPage && StartPage && (
|
||||
<div className={'grafana-info-box grafana-info-box--max-lg'}>
|
||||
@ -379,6 +391,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onStopScanning={this.onStopScanning}
|
||||
/>
|
||||
)}
|
||||
{mode === ExploreMode.Tracing && (
|
||||
<div className={styles.fullHeight}>
|
||||
{queryResponse &&
|
||||
!!queryResponse.series.length &&
|
||||
queryResponse.series[0].fields[0].values.get(0) && (
|
||||
<iframe
|
||||
className={styles.iframe}
|
||||
src={queryResponse.series[0].fields[0].values.get(0)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showRichHistory && (
|
||||
@ -448,7 +472,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
||||
newMode = supportedModes[0];
|
||||
}
|
||||
} else {
|
||||
newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(urlMode) ? urlMode : undefined;
|
||||
newMode = [ExploreMode.Metrics, ExploreMode.Logs, ExploreMode.Tracing].includes(urlMode) ? urlMode : undefined;
|
||||
}
|
||||
|
||||
const initialUI = ui || DEFAULT_UI_STATE;
|
||||
|
@ -366,7 +366,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
containerWidth,
|
||||
} = exploreItem;
|
||||
|
||||
const hasLiveOption = datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs;
|
||||
const hasLiveOption = !!(datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs);
|
||||
|
||||
return {
|
||||
datasourceMissing,
|
||||
|
@ -14,12 +14,13 @@ import {
|
||||
TimeRange,
|
||||
LogsMetaItem,
|
||||
GraphSeriesXY,
|
||||
Field,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { changeDedupStrategy, updateTimeRange } from './state/actions';
|
||||
import { changeDedupStrategy, updateTimeRange, splitOpen } from './state/actions';
|
||||
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
||||
import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors';
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
@ -57,6 +58,7 @@ interface LogsContainerProps {
|
||||
syncedTimes: boolean;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
isPaused: boolean;
|
||||
splitOpen: typeof splitOpen;
|
||||
}
|
||||
|
||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
@ -87,6 +89,30 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get links from the filed of a dataframe that was given to as and in addition check if there is associated
|
||||
* metadata with datasource in which case we will add onClick to open the link in new split window. This assumes
|
||||
* that we just supply datasource name and field value and Explore split window will know how to render that
|
||||
* appropriately. This is for example used for transition from log with traceId to trace datasource to show that
|
||||
* trace.
|
||||
* @param field
|
||||
* @param rowIndex
|
||||
*/
|
||||
getFieldLinks = (field: Field, rowIndex: number) => {
|
||||
const data = getLinksFromLogsField(field, rowIndex);
|
||||
return data.map(d => {
|
||||
if (d.link.meta?.datasourceName) {
|
||||
return {
|
||||
...d.linkModel,
|
||||
onClick: () => {
|
||||
this.props.splitOpen(d.link.meta.datasourceName, field.values.get(rowIndex));
|
||||
},
|
||||
};
|
||||
}
|
||||
return d.linkModel;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
@ -149,7 +175,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
scanRange={range.raw}
|
||||
width={width}
|
||||
getRowContext={this.getLogRowContext}
|
||||
getFieldLinks={getLinksFromLogsField}
|
||||
getFieldLinks={this.getFieldLinks}
|
||||
/>
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
@ -199,6 +225,7 @@ const mapDispatchToProps = {
|
||||
changeDedupStrategy,
|
||||
toggleLogLevelAction,
|
||||
updateTimeRange,
|
||||
splitOpen,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
|
||||
|
@ -46,7 +46,7 @@ export class TableContainer extends PureComponent<TableContainerProps> {
|
||||
return (
|
||||
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||
{hasTableResult ? (
|
||||
<Table data={tableResult} width={tableWidth} height={height} onCellClick={onClickCell} />
|
||||
<Table data={tableResult!} width={tableWidth} height={height} onCellClick={onClickCell} />
|
||||
) : (
|
||||
<MetaInfoText metaItems={[{ value: '0 series returned' }]} />
|
||||
)}
|
||||
|
@ -5,9 +5,9 @@ import { connect } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import Explore from './Explore';
|
||||
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
|
||||
import { resetExploreAction } from './state/actionTypes';
|
||||
import Explore from './Explore';
|
||||
|
||||
interface WrapperProps {
|
||||
split: boolean;
|
||||
@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> {
|
||||
return (
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page">
|
||||
<div className="explore-wrapper">
|
||||
<div style={{ height: '100%' }} className="explore-wrapper">
|
||||
<ErrorBoundaryAlert style="page">
|
||||
<Explore exploreId={ExploreId.left} />
|
||||
</ErrorBoundaryAlert>
|
||||
|
@ -123,7 +123,7 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
|
||||
*/
|
||||
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
let newDataSourceInstance: DataSourceApi = null;
|
||||
let newDataSourceInstance: DataSourceApi;
|
||||
|
||||
if (!datasource) {
|
||||
newDataSourceInstance = await getDatasourceSrv().get();
|
||||
@ -317,7 +317,7 @@ export const loadDatasourceReady = (
|
||||
instance: DataSourceApi,
|
||||
orgId: number
|
||||
): PayloadAction<LoadDatasourceReadyPayload> => {
|
||||
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
||||
const historyKey = `grafana.explore.history.${instance.meta?.id}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
// Save last-used datasource
|
||||
|
||||
@ -340,7 +340,7 @@ export const loadDatasourceReady = (
|
||||
export const importQueries = (
|
||||
exploreId: ExploreId,
|
||||
queries: DataQuery[],
|
||||
sourceDataSource: DataSourceApi,
|
||||
sourceDataSource: DataSourceApi | undefined,
|
||||
targetDataSource: DataSourceApi
|
||||
): ThunkResult<void> => {
|
||||
return async dispatch => {
|
||||
@ -352,7 +352,7 @@ export const importQueries = (
|
||||
|
||||
let importedQueries = queries;
|
||||
// Check if queries can be imported from previously selected datasource
|
||||
if (sourceDataSource.meta.id === targetDataSource.meta.id) {
|
||||
if (sourceDataSource.meta?.id === targetDataSource.meta?.id) {
|
||||
// Keep same queries if same type of datasource
|
||||
importedQueries = [...queries];
|
||||
} else if (targetDataSource.importQueries) {
|
||||
@ -701,18 +701,31 @@ export function splitClose(itemId: ExploreId): ThunkResult<void> {
|
||||
* The right state is automatically initialized.
|
||||
* The copy keeps all query modifications but wipes the query results.
|
||||
*/
|
||||
export function splitOpen(): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
export function splitOpen(dataSourceName?: string, query?: string): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
// Clone left state to become the right state
|
||||
const leftState = getState().explore[ExploreId.left];
|
||||
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
||||
const rightState: ExploreItemState = {
|
||||
...leftState,
|
||||
};
|
||||
const queryState = getState().location.query[ExploreId.left] as string;
|
||||
const urlState = parseUrlState(queryState);
|
||||
const itemState: ExploreItemState = {
|
||||
...leftState,
|
||||
queries: leftState.queries.slice(),
|
||||
urlState,
|
||||
};
|
||||
dispatch(splitOpenAction({ itemState }));
|
||||
rightState.queries = leftState.queries.slice();
|
||||
rightState.urlState = urlState;
|
||||
dispatch(splitOpenAction({ itemState: rightState }));
|
||||
|
||||
if (dataSourceName && query) {
|
||||
// This is hardcoded for Jaeger right now
|
||||
const queries = [
|
||||
{
|
||||
query,
|
||||
refId: 'A',
|
||||
} as DataQuery,
|
||||
];
|
||||
await dispatch(changeDatasource(ExploreId.right, dataSourceName));
|
||||
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
||||
}
|
||||
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
@ -757,7 +770,8 @@ const togglePanelActionCreator = (
|
||||
}
|
||||
|
||||
dispatch(actionCreator({ exploreId }));
|
||||
dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
|
||||
// The switch further up is exhaustive so uiFragmentStateUpdate should definitely be initialized
|
||||
dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate!));
|
||||
|
||||
if (shouldRunQueries) {
|
||||
dispatch(runQueries(exploreId));
|
||||
|
@ -599,6 +599,7 @@ export const updateChildRefreshState = (
|
||||
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => {
|
||||
const supportsGraph = dataSource.meta.metrics;
|
||||
const supportsLogs = dataSource.meta.logs;
|
||||
const supportsTracing = dataSource.meta.tracing;
|
||||
|
||||
let mode = currentMode || ExploreMode.Metrics;
|
||||
const supportedModes: ExploreMode[] = [];
|
||||
@ -611,13 +612,17 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo
|
||||
supportedModes.push(ExploreMode.Logs);
|
||||
}
|
||||
|
||||
if (supportsTracing) {
|
||||
supportedModes.push(ExploreMode.Tracing);
|
||||
}
|
||||
|
||||
if (supportedModes.length === 1) {
|
||||
mode = supportedModes[0];
|
||||
}
|
||||
|
||||
// HACK: Used to set Loki's default explore mode to Logs mode.
|
||||
// A better solution would be to introduce a "default" or "preferred" mode to the datasource config
|
||||
if (dataSource.meta.name === 'Loki' && !currentMode) {
|
||||
if (dataSource.meta.name === 'Loki' && (!currentMode || supportedModes.indexOf(currentMode) === -1)) {
|
||||
mode = ExploreMode.Logs;
|
||||
}
|
||||
|
||||
|
@ -54,8 +54,8 @@ describe('getLinksFromLogsField', () => {
|
||||
};
|
||||
const links = getLinksFromLogsField(field, 2);
|
||||
expect(links.length).toBe(2);
|
||||
expect(links[0].href).toBe('http://domain.com/3');
|
||||
expect(links[1].href).toBe('http://anotherdomain.sk/3');
|
||||
expect(links[0].linkModel.href).toBe('http://domain.com/3');
|
||||
expect(links[1].linkModel.href).toBe('http://anotherdomain.sk/3');
|
||||
});
|
||||
|
||||
it('handles zero links', () => {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
LinkModel,
|
||||
formattedValueToString,
|
||||
DisplayValue,
|
||||
DataLink,
|
||||
} from '@grafana/data';
|
||||
import { getLinkSrv } from './link_srv';
|
||||
import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy';
|
||||
@ -143,7 +144,10 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane
|
||||
};
|
||||
};
|
||||
|
||||
export const getLinksFromLogsField = (field: Field, rowIndex: number): Array<LinkModel<Field>> => {
|
||||
export const getLinksFromLogsField = (
|
||||
field: Field,
|
||||
rowIndex: number
|
||||
): Array<{ linkModel: LinkModel<Field>; link: DataLink }> => {
|
||||
const scopedVars: any = {};
|
||||
scopedVars['__value'] = {
|
||||
value: {
|
||||
@ -153,6 +157,11 @@ export const getLinksFromLogsField = (field: Field, rowIndex: number): Array<Lin
|
||||
};
|
||||
|
||||
return field.config.links
|
||||
? field.config.links.map(link => getLinkSrv().getDataLinkUIModel(link, scopedVars, field))
|
||||
? field.config.links.map(link => {
|
||||
return {
|
||||
link,
|
||||
linkModel: getLinkSrv().getDataLinkUIModel(link, scopedVars, field),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
};
|
||||
|
@ -13,6 +13,8 @@ const grafanaPlugin = async () =>
|
||||
const influxdbPlugin = async () =>
|
||||
await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module');
|
||||
const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module');
|
||||
const jaegerPlugin = async () =>
|
||||
await import(/* webpackChunkName: "jaegerPlugin" */ 'app/plugins/datasource/jaeger/module');
|
||||
const mixedPlugin = async () =>
|
||||
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
|
||||
const mysqlPlugin = async () =>
|
||||
@ -64,6 +66,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/datasource/grafana/module': grafanaPlugin,
|
||||
'app/plugins/datasource/influxdb/module': influxdbPlugin,
|
||||
'app/plugins/datasource/loki/module': lokiPlugin,
|
||||
'app/plugins/datasource/jaeger/module': jaegerPlugin,
|
||||
'app/plugins/datasource/mixed/module': mixedPlugin,
|
||||
'app/plugins/datasource/mysql/module': mysqlPlugin,
|
||||
'app/plugins/datasource/postgres/module': postgresPlugin,
|
||||
|
18
public/app/plugins/datasource/jaeger/ConfigEditor.tsx
Normal file
18
public/app/plugins/datasource/jaeger/ConfigEditor.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
||||
export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
|
||||
return (
|
||||
<>
|
||||
<DataSourceHttpSettings
|
||||
defaultUrl={'http://localhost:16686'}
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={true}
|
||||
onChange={onOptionsChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
202
public/app/plugins/datasource/jaeger/QueryField.tsx
Normal file
202
public/app/plugins/datasource/jaeger/QueryField.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import { JaegerDatasource, JaegerQuery } from './datasource';
|
||||
import { ButtonCascader, CascaderOption } from '@grafana/ui';
|
||||
|
||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||
|
||||
const ALL_OPERATIONS_KEY = '__ALL__';
|
||||
const NO_TRACES_KEY = '__NO_TRACES__';
|
||||
|
||||
type Props = ExploreQueryFieldProps<JaegerDatasource, JaegerQuery>;
|
||||
interface State {
|
||||
serviceOptions: CascaderOption[];
|
||||
}
|
||||
|
||||
function getLabelFromTrace(trace: any): string {
|
||||
const firstSpan = trace.spans && trace.spans[0];
|
||||
if (firstSpan) {
|
||||
return `${firstSpan.operationName} [${firstSpan.duration} ms]`;
|
||||
}
|
||||
return trace.traceID;
|
||||
}
|
||||
|
||||
export class JaegerQueryField extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props, context: React.Context<any>) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
serviceOptions: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getServices();
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
const url = '/api/services';
|
||||
const { datasource } = this.props;
|
||||
try {
|
||||
const res = await datasource.metadataRequest(url);
|
||||
if (res) {
|
||||
const services = res as string[];
|
||||
const serviceOptions: CascaderOption[] = services.sort().map(service => ({
|
||||
label: service,
|
||||
value: service,
|
||||
isLeaf: false,
|
||||
}));
|
||||
this.setState({ serviceOptions });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onLoadOptions = async (selectedOptions: CascaderOption[]) => {
|
||||
const service = selectedOptions[0].value;
|
||||
if (selectedOptions.length === 1) {
|
||||
// Load operations
|
||||
const operations: string[] = await this.findOperations(service);
|
||||
const allOperationsOption: CascaderOption = {
|
||||
label: '[ALL]',
|
||||
value: ALL_OPERATIONS_KEY,
|
||||
};
|
||||
const operationOptions: CascaderOption[] = [
|
||||
allOperationsOption,
|
||||
...operations.sort().map(operation => ({
|
||||
label: operation,
|
||||
value: operation,
|
||||
isLeaf: false,
|
||||
})),
|
||||
];
|
||||
this.setState(state => {
|
||||
const serviceOptions = state.serviceOptions.map(serviceOption => {
|
||||
if (serviceOption.value === service) {
|
||||
return {
|
||||
...serviceOption,
|
||||
children: operationOptions,
|
||||
};
|
||||
}
|
||||
return serviceOption;
|
||||
});
|
||||
return { serviceOptions };
|
||||
});
|
||||
} else if (selectedOptions.length === 2) {
|
||||
// Load traces
|
||||
const operationValue = selectedOptions[1].value;
|
||||
const operation = operationValue === ALL_OPERATIONS_KEY ? '' : operationValue;
|
||||
const traces: any[] = await this.findTraces(service, operation);
|
||||
let traceOptions: CascaderOption[] = traces.map(trace => ({
|
||||
label: getLabelFromTrace(trace),
|
||||
value: trace.traceID,
|
||||
}));
|
||||
if (traceOptions.length === 0) {
|
||||
traceOptions = [
|
||||
{
|
||||
label: '[No traces in time range]',
|
||||
value: NO_TRACES_KEY,
|
||||
},
|
||||
];
|
||||
}
|
||||
this.setState(state => {
|
||||
// Place new traces into the correct service/operation sub-tree
|
||||
const serviceOptions = state.serviceOptions.map(serviceOption => {
|
||||
if (serviceOption.value === service) {
|
||||
const operationOptions = serviceOption.children.map(operationOption => {
|
||||
if (operationOption.value === operationValue) {
|
||||
return {
|
||||
...operationOption,
|
||||
children: traceOptions,
|
||||
};
|
||||
}
|
||||
return operationOption;
|
||||
});
|
||||
return {
|
||||
...serviceOption,
|
||||
children: operationOptions,
|
||||
};
|
||||
}
|
||||
return serviceOption;
|
||||
});
|
||||
return { serviceOptions };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findOperations = async (service: string) => {
|
||||
const { datasource } = this.props;
|
||||
const url = `/api/services/${service}/operations`;
|
||||
try {
|
||||
return await datasource.metadataRequest(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
findTraces = async (service: string, operation?: string) => {
|
||||
const { datasource } = this.props;
|
||||
const { start, end } = datasource.getTimeRange();
|
||||
|
||||
const traceSearch = {
|
||||
start,
|
||||
end,
|
||||
service,
|
||||
operation,
|
||||
limit: 10,
|
||||
lookback: '1h',
|
||||
maxDuration: '',
|
||||
minDuration: '',
|
||||
};
|
||||
const url = '/api/traces';
|
||||
try {
|
||||
return await datasource.metadataRequest(url, traceSearch);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
onSelectTrace = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
const { query, onChange, onRunQuery } = this.props;
|
||||
if (selectedOptions.length === 3) {
|
||||
const traceID = selectedOptions[2].value;
|
||||
onChange({ ...query, query: traceID });
|
||||
onRunQuery();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query, onChange } = this.props;
|
||||
const { serviceOptions } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||
<div className="gf-form flex-shrink-0">
|
||||
<ButtonCascader options={serviceOptions} onChange={this.onSelectTrace} loadData={this.onLoadOptions}>
|
||||
Traces
|
||||
</ButtonCascader>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow flex-shrink-1">
|
||||
<div className={'slate-query-field__wrapper'}>
|
||||
<div className="slate-query-field">
|
||||
<input
|
||||
style={{ width: '100%' }}
|
||||
value={query.query || ''}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...query,
|
||||
query: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default JaegerQueryField;
|
85
public/app/plugins/datasource/jaeger/datasource.ts
Normal file
85
public/app/plugins/datasource/jaeger/datasource.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
dateMath,
|
||||
DateTime,
|
||||
MutableDataFrame,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataQuery,
|
||||
} from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { DatasourceRequestOptions } from 'app/core/services/backend_srv';
|
||||
import { serializeParams } from '../../../core/utils/fetch';
|
||||
|
||||
import { Observable, from, of } from 'rxjs';
|
||||
|
||||
export type JaegerQuery = {
|
||||
query: string;
|
||||
} & DataQuery;
|
||||
|
||||
export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
|
||||
constructor(private instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
_request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> {
|
||||
// Hack for proxying metadata requests
|
||||
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
|
||||
const params = data ? serializeParams(data) : '';
|
||||
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
|
||||
const req = {
|
||||
...options,
|
||||
url,
|
||||
};
|
||||
|
||||
return from(getBackendSrv().datasourceRequest(req));
|
||||
}
|
||||
|
||||
async metadataRequest(url: string, params?: Record<string, any>) {
|
||||
const res = await this._request(url, params, { silent: true }).toPromise();
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> {
|
||||
//http://localhost:16686/search?end=1573338717880000&limit=20&lookback=6h&maxDuration&minDuration&service=app&start=1573317117880000
|
||||
const url =
|
||||
options.targets.length && options.targets[0].query
|
||||
? `${this.instanceSettings.url}/trace/${options.targets[0].query}?uiEmbed=v0`
|
||||
: '';
|
||||
|
||||
return of({
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
values: [url],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async testDatasource(): Promise<any> {
|
||||
return true;
|
||||
}
|
||||
|
||||
getTime(date: string | DateTime, roundUp: boolean) {
|
||||
if (typeof date === 'string') {
|
||||
date = dateMath.parse(date, roundUp);
|
||||
}
|
||||
return date.valueOf() * 1000;
|
||||
}
|
||||
|
||||
getTimeRange(): { start: number; end: number } {
|
||||
const range = getTimeSrv().timeRange();
|
||||
return {
|
||||
start: this.getTime(range.from, false),
|
||||
end: this.getTime(range.to, true),
|
||||
};
|
||||
}
|
||||
}
|
236
public/app/plugins/datasource/jaeger/img/jaeger_logo.svg
Normal file
236
public/app/plugins/datasource/jaeger/img/jaeger_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 34 KiB |
8
public/app/plugins/datasource/jaeger/module.ts
Normal file
8
public/app/plugins/datasource/jaeger/module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { JaegerDatasource } from './datasource';
|
||||
import { JaegerQueryField } from './QueryField';
|
||||
import { ConfigEditor } from './ConfigEditor';
|
||||
|
||||
export const plugin = new DataSourcePlugin(JaegerDatasource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setExploreQueryField(JaegerQueryField);
|
35
public/app/plugins/datasource/jaeger/plugin.json
Normal file
35
public/app/plugins/datasource/jaeger/plugin.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Jaeger",
|
||||
"id": "jaeger",
|
||||
"category": "tracing",
|
||||
|
||||
"metrics": false,
|
||||
"alerting": false,
|
||||
"annotations": false,
|
||||
"logs": false,
|
||||
"streaming": false,
|
||||
"tracing": true,
|
||||
|
||||
"info": {
|
||||
"description": "Open source, end-to-end distributed tracing",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/jaeger_logo.svg",
|
||||
"large": "img/jaeger_logo.svg"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"name": "Learn more",
|
||||
"url": "https://www.jaegertracing.io"
|
||||
},
|
||||
{
|
||||
"name": "GitHub Project",
|
||||
"url": "https://github.com/jaegertracing/jaeger"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import cx from 'classnames';
|
||||
import { FormField } from '@grafana/ui';
|
||||
import { DerivedFieldConfig } from '../types';
|
||||
import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers';
|
||||
import { ArrayVector, FieldType } from '@grafana/data';
|
||||
import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data';
|
||||
|
||||
type Props = {
|
||||
derivedFields: DerivedFieldConfig[];
|
||||
@ -90,7 +90,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
||||
try {
|
||||
const testMatch = debugText.match(field.matcherRegex);
|
||||
const value = testMatch && testMatch[1];
|
||||
let link;
|
||||
let link: LinkModel<Field>;
|
||||
|
||||
if (field.url && value) {
|
||||
link = getLinksFromLogsField(
|
||||
@ -103,7 +103,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
||||
},
|
||||
},
|
||||
0
|
||||
)[0];
|
||||
)[0].linkModel;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { Button, FormField, DataLinkInput, stylesFactory } from '@grafana/ui';
|
||||
import { Button, FormField, DataLinkInput, stylesFactory, Switch } from '@grafana/ui';
|
||||
import { VariableSuggestion } from '@grafana/data';
|
||||
import { DataSourceSelectItem } from '@grafana/data';
|
||||
|
||||
import { DerivedFieldConfig } from '../types';
|
||||
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
const getStyles = stylesFactory(() => ({
|
||||
firstRow: css`
|
||||
row: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
`,
|
||||
@ -27,6 +32,7 @@ type Props = {
|
||||
export const DerivedField = (props: Props) => {
|
||||
const { value, onChange, onDelete, suggestions, className } = props;
|
||||
const styles = getStyles();
|
||||
const [hasIntenalLink, setHasInternalLink] = useState(!!value.datasourceName);
|
||||
|
||||
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
@ -37,7 +43,7 @@ export const DerivedField = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={styles.firstRow}>
|
||||
<div className={styles.row}>
|
||||
<FormField
|
||||
className={styles.nameField}
|
||||
labelWidth={5}
|
||||
@ -93,6 +99,64 @@ export const DerivedField = (props: Props) => {
|
||||
width: 100%;
|
||||
`}
|
||||
/>
|
||||
|
||||
{config.featureToggles.tracingIntegration && (
|
||||
<div className={styles.row}>
|
||||
<Switch
|
||||
label="Internal link"
|
||||
checked={hasIntenalLink}
|
||||
onChange={() => {
|
||||
if (hasIntenalLink) {
|
||||
onChange({
|
||||
...value,
|
||||
datasourceName: undefined,
|
||||
});
|
||||
}
|
||||
setHasInternalLink(!hasIntenalLink);
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasIntenalLink && (
|
||||
<DataSourceSection
|
||||
onChange={datasourceName => {
|
||||
onChange({
|
||||
...value,
|
||||
datasourceName,
|
||||
});
|
||||
}}
|
||||
datasourceName={value.datasourceName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type DataSourceSectionProps = {
|
||||
datasourceName?: string;
|
||||
onChange: (name: string) => void;
|
||||
};
|
||||
const DataSourceSection = (props: DataSourceSectionProps) => {
|
||||
const { datasourceName, onChange } = props;
|
||||
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||
.getExternal()
|
||||
.map(
|
||||
(ds: any) =>
|
||||
({
|
||||
value: ds.name,
|
||||
name: ds.name,
|
||||
meta: ds.meta,
|
||||
} as DataSourceSelectItem)
|
||||
);
|
||||
const selectedDatasource = datasourceName && datasources.find(d => d.name === datasourceName);
|
||||
return (
|
||||
<DataSourcePicker
|
||||
onChange={newValue => {
|
||||
onChange(newValue.name);
|
||||
}}
|
||||
datasources={datasources}
|
||||
current={selectedDatasource}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
} from './types';
|
||||
import { LegacyTarget, LiveStreams } from './live_streams';
|
||||
import LanguageProvider from './language_provider';
|
||||
import { serializeParams } from '../../../core/utils/fetch';
|
||||
|
||||
export type RangeQueryOptions = Pick<DataQueryRequest<LokiQuery>, 'range' | 'intervalMs' | 'maxDataPoints' | 'reverse'>;
|
||||
export const DEFAULT_MAX_LINES = 1000;
|
||||
@ -68,12 +69,6 @@ const DEFAULT_QUERY_PARAMS: Partial<LokiLegacyQueryRequest> = {
|
||||
query: '',
|
||||
};
|
||||
|
||||
function serializeParams(data: Record<string, any>) {
|
||||
return Object.keys(data)
|
||||
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
interface LokiContextQueryOptions {
|
||||
direction?: 'BACKWARD' | 'FORWARD';
|
||||
limit?: number;
|
||||
|
@ -395,11 +395,16 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul
|
||||
|
||||
const fields = derivedFields.reduce((acc, field) => {
|
||||
const config: FieldConfig = {};
|
||||
if (field.url) {
|
||||
if (field.url || field.datasourceName) {
|
||||
config.links = [
|
||||
{
|
||||
url: field.url,
|
||||
title: '',
|
||||
meta: field.datasourceName
|
||||
? {
|
||||
datasourceName: field.datasourceName,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -127,6 +127,7 @@ export type DerivedFieldConfig = {
|
||||
matcherRegex: string;
|
||||
name: string;
|
||||
url?: string;
|
||||
datasourceName?: string;
|
||||
};
|
||||
|
||||
export interface TransformerOptions {
|
||||
|
16
public/app/plugins/datasource/zipkin/ConfigEditor.tsx
Normal file
16
public/app/plugins/datasource/zipkin/ConfigEditor.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
||||
export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
|
||||
return (
|
||||
<DataSourceHttpSettings
|
||||
defaultUrl={'http://localhost:3100'}
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={true}
|
||||
onChange={onOptionsChange}
|
||||
/>
|
||||
);
|
||||
};
|
22
public/app/plugins/datasource/zipkin/QueryField.tsx
Normal file
22
public/app/plugins/datasource/zipkin/QueryField.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { ZipkinDatasource, ZipkinQuery } from './datasource';
|
||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||
|
||||
type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>;
|
||||
|
||||
export const QueryField = (props: Props) => (
|
||||
<div className={'slate-query-field__wrapper'}>
|
||||
<div className="slate-query-field">
|
||||
<input
|
||||
style={{ width: '100%' }}
|
||||
value={props.query.query || ''}
|
||||
onChange={e =>
|
||||
props.onChange({
|
||||
...props.query,
|
||||
query: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
38
public/app/plugins/datasource/zipkin/datasource.ts
Normal file
38
public/app/plugins/datasource/zipkin/datasource.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {
|
||||
MutableDataFrame,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataQuery,
|
||||
} from '@grafana/data';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
export type ZipkinQuery = {
|
||||
query: string;
|
||||
} & DataQuery;
|
||||
|
||||
export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
|
||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<ZipkinQuery>): Observable<DataQueryResponse> {
|
||||
return of({
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async testDatasource(): Promise<any> {
|
||||
return true;
|
||||
}
|
||||
}
|
1
public/app/plugins/datasource/zipkin/img/zipkin-logo.svg
Normal file
1
public/app/plugins/datasource/zipkin/img/zipkin-logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 20 KiB |
8
public/app/plugins/datasource/zipkin/module.ts
Normal file
8
public/app/plugins/datasource/zipkin/module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { ZipkinDatasource } from './datasource';
|
||||
import { QueryField } from './QueryField';
|
||||
import { ConfigEditor } from './ConfigEditor';
|
||||
|
||||
export const plugin = new DataSourcePlugin(ZipkinDatasource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setExploreQueryField(QueryField);
|
31
public/app/plugins/datasource/zipkin/plugin.json
Normal file
31
public/app/plugins/datasource/zipkin/plugin.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Zipkin",
|
||||
"id": "zipkin",
|
||||
"category": "tracing",
|
||||
|
||||
"metrics": false,
|
||||
"alerting": false,
|
||||
"annotations": false,
|
||||
"logs": false,
|
||||
"streaming": false,
|
||||
"tracing": true,
|
||||
|
||||
"info": {
|
||||
"description": "Placeholder for the distributed tracing system.",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/zipkin-logo.svg",
|
||||
"large": "img/zipkin-logo.svg"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"name": "Learn more",
|
||||
"url": "https://zipkin.io"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user