Commit ae09ccbf by Andrej Ocenas Committed by GitHub

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>
parent b6f73e35
...@@ -26,7 +26,11 @@ export namespace dateMath { ...@@ -26,7 +26,11 @@ export namespace dateMath {
* @param roundUp See parseDateMath function. * @param roundUp See parseDateMath function.
* @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used. * @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) { if (!text) {
return undefined; return undefined;
} }
......
...@@ -27,6 +27,9 @@ export interface DataLink { ...@@ -27,6 +27,9 @@ export interface DataLink {
// 1: If exists, handle click directly // 1: If exists, handle click directly
// Not saved in JSON/DTO // Not saved in JSON/DTO
onClick?: (event: DataLinkClickEvent) => void; 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'; export type LinkTarget = '_blank' | '_self';
......
...@@ -115,6 +115,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet ...@@ -115,6 +115,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet
logs?: boolean; logs?: boolean;
annotations?: boolean; annotations?: boolean;
alerting?: boolean; alerting?: boolean;
tracing?: boolean;
mixed?: boolean; mixed?: boolean;
hasQueryHelp?: boolean; hasQueryHelp?: boolean;
category?: string; category?: string;
...@@ -316,6 +317,7 @@ export enum DataSourceStatus { ...@@ -316,6 +317,7 @@ export enum DataSourceStatus {
export enum ExploreMode { export enum ExploreMode {
Logs = 'Logs', Logs = 'Logs',
Metrics = 'Metrics', Metrics = 'Metrics',
Tracing = 'Tracing',
} }
export interface ExploreQueryFieldProps< export interface ExploreQueryFieldProps<
......
...@@ -19,6 +19,7 @@ interface FeatureToggles { ...@@ -19,6 +19,7 @@ interface FeatureToggles {
newEdit: boolean; newEdit: boolean;
meta: boolean; meta: boolean;
newVariables: boolean; newVariables: boolean;
tracingIntegration: boolean;
} }
interface LicenseInfo { interface LicenseInfo {
...@@ -71,6 +72,7 @@ export class GrafanaBootConfig { ...@@ -71,6 +72,7 @@ export class GrafanaBootConfig {
newEdit: false, newEdit: false,
meta: false, meta: false,
newVariables: false, newVariables: false,
tracingIntegration: false,
}; };
licenseInfo: LicenseInfo = {} as LicenseInfo; licenseInfo: LicenseInfo = {} as LicenseInfo;
phantomJSRenderer = false; phantomJSRenderer = false;
......
...@@ -24,7 +24,7 @@ import { LogDetailsRow } from './LogDetailsRow'; ...@@ -24,7 +24,7 @@ import { LogDetailsRow } from './LogDetailsRow';
type FieldDef = { type FieldDef = {
key: string; key: string;
value: string; value: string;
links?: string[]; links?: Array<LinkModel<Field>>;
fieldIndex?: number; fieldIndex?: number;
}; };
...@@ -99,7 +99,7 @@ class UnThemedLogDetails extends PureComponent<Props> { ...@@ -99,7 +99,7 @@ class UnThemedLogDetails extends PureComponent<Props> {
return { return {
key: field.name, key: field.name,
value: field.values.get(row.rowIndex).toString(), value: field.values.get(row.rowIndex).toString(),
links: links.map(link => link.href), links: links,
fieldIndex: field.index, fieldIndex: field.index,
}; };
}) })
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { css, cx } from 'emotion'; 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 { Themeable } from '../../types/theme';
import { withTheme } from '../../themes/index'; import { withTheme } from '../../themes/index';
...@@ -9,6 +9,7 @@ import { stylesFactory } from '../../themes/stylesFactory'; ...@@ -9,6 +9,7 @@ import { stylesFactory } from '../../themes/stylesFactory';
//Components //Components
import { LogLabelStats } from './LogLabelStats'; import { LogLabelStats } from './LogLabelStats';
import { LinkButton } from '../Button/Button';
export interface Props extends Themeable { export interface Props extends Themeable {
parsedValue: string; parsedValue: string;
...@@ -16,7 +17,7 @@ export interface Props extends Themeable { ...@@ -16,7 +17,7 @@ export interface Props extends Themeable {
isLabel?: boolean; isLabel?: boolean;
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
links?: string[]; links?: Array<LinkModel<Field>>;
getStats: () => LogLabelStatsModel[] | null; getStats: () => LogLabelStatsModel[] | null;
} }
...@@ -122,11 +123,27 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> { ...@@ -122,11 +123,27 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
{links && {links &&
links.map(link => { links.map(link => {
return ( return (
<span key={link}> <span key={link.href}>
&nbsp; <>
<a href={link} target={'_blank'}> &nbsp;
<i className={'fa fa-external-link'} /> <LinkButton
</a> 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> </span>
); );
})} })}
......
...@@ -92,7 +92,11 @@ func pluginScenario(desc string, t *testing.T, fn func()) { ...@@ -92,7 +92,11 @@ func pluginScenario(desc string, t *testing.T, fn func()) {
_, err := sec.NewKey("path", "testdata/test-app") _, err := sec.NewKey("path", "testdata/test-app")
So(err, ShouldBeNil) So(err, ShouldBeNil)
pm := &PluginManager{} pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
},
}
err = pm.Init() err = pm.Init()
So(err, ShouldBeNil) So(err, ShouldBeNil)
......
...@@ -18,7 +18,11 @@ func TestPluginDashboards(t *testing.T) { ...@@ -18,7 +18,11 @@ func TestPluginDashboards(t *testing.T) {
_, err := sec.NewKey("path", "testdata/test-app") _, err := sec.NewKey("path", "testdata/test-app")
So(err, ShouldBeNil) So(err, ShouldBeNil)
pm := &PluginManager{} pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
},
}
err = pm.Init() err = pm.Init()
So(err, ShouldBeNil) So(err, ShouldBeNil)
......
...@@ -23,6 +23,7 @@ type DataSourcePlugin struct { ...@@ -23,6 +23,7 @@ type DataSourcePlugin struct {
Explore bool `json:"explore"` Explore bool `json:"explore"`
Table bool `json:"tables"` Table bool `json:"tables"`
Logs bool `json:"logs"` Logs bool `json:"logs"`
Tracing bool `json:"tracing"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"` QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"` BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"` Mixed bool `json:"mixed,omitempty"`
......
...@@ -42,10 +42,12 @@ type PluginScanner struct { ...@@ -42,10 +42,12 @@ type PluginScanner struct {
pluginPath string pluginPath string
errors []error errors []error
backendPluginManager backendplugin.Manager backendPluginManager backendplugin.Manager
cfg *setting.Cfg
} }
type PluginManager struct { type PluginManager struct {
BackendPluginManager backendplugin.Manager `inject:""` BackendPluginManager backendplugin.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger log log.Logger
} }
...@@ -164,6 +166,7 @@ func (pm *PluginManager) scan(pluginDir string) error { ...@@ -164,6 +166,7 @@ func (pm *PluginManager) scan(pluginDir string) error {
scanner := &PluginScanner{ scanner := &PluginScanner{
pluginPath: pluginDir, pluginPath: pluginDir,
backendPluginManager: pm.BackendPluginManager, backendPluginManager: pm.BackendPluginManager,
cfg: pm.Cfg,
} }
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil { 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 ...@@ -213,6 +216,14 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
return nil 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" { if f.Name() == "plugin.json" {
err := scanner.loadPluginJson(currentPath) err := scanner.loadPluginJson(currentPath)
if err != nil { if err != nil {
......
...@@ -15,7 +15,11 @@ func TestPluginScans(t *testing.T) { ...@@ -15,7 +15,11 @@ func TestPluginScans(t *testing.T) {
setting.StaticRootPath, _ = filepath.Abs("../../public/") setting.StaticRootPath, _ = filepath.Abs("../../public/")
setting.Raw = ini.Empty() setting.Raw = ini.Empty()
pm := &PluginManager{} pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
},
}
err := pm.Init() err := pm.Init()
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -34,7 +38,11 @@ func TestPluginScans(t *testing.T) { ...@@ -34,7 +38,11 @@ func TestPluginScans(t *testing.T) {
_, err = sec.NewKey("path", "testdata/test-app") _, err = sec.NewKey("path", "testdata/test-app")
So(err, ShouldBeNil) So(err, ShouldBeNil)
pm := &PluginManager{} pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
},
}
err = pm.Init() err = pm.Init()
So(err, ShouldBeNil) So(err, ShouldBeNil)
......
...@@ -281,6 +281,7 @@ type Cfg struct { ...@@ -281,6 +281,7 @@ type Cfg struct {
ApiKeyMaxSecondsToLive int64 ApiKeyMaxSecondsToLive int64
// Use to enable new features which may still be in alpha/beta stage.
FeatureToggles map[string]bool FeatureToggles map[string]bool
} }
......
...@@ -66,17 +66,17 @@ export interface GetExploreUrlArguments { ...@@ -66,17 +66,17 @@ export interface GetExploreUrlArguments {
datasourceSrv: DataSourceSrv; datasourceSrv: DataSourceSrv;
timeSrv: TimeSrv; timeSrv: TimeSrv;
} }
export async function getExploreUrl(args: GetExploreUrlArguments) { export async function getExploreUrl(args: GetExploreUrlArguments): Promise<string | undefined> {
const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args; const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args;
let exploreDatasource = panelDatasource; let exploreDatasource = panelDatasource;
let exploreTargets: DataQuery[] = panelTargets; let exploreTargets: DataQuery[] = panelTargets;
let url: string; let url: string | undefined;
// Mixed datasources need to choose only one datasource // 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 // Find first explore datasource among targets
for (const t of exploreTargets) { for (const t of exploreTargets) {
const datasource = await datasourceSrv.get(t.datasource); const datasource = await datasourceSrv.get(t.datasource || undefined);
if (datasource) { if (datasource) {
exploreDatasource = datasource; exploreDatasource = datasource;
exploreTargets = panelTargets.filter(t => t.datasource === datasource.name); exploreTargets = panelTargets.filter(t => t.datasource === datasource.name);
...@@ -183,7 +183,7 @@ enum ParseUiStateIndex { ...@@ -183,7 +183,7 @@ enum ParseUiStateIndex {
Strategy = 3, Strategy = 3,
} }
export const safeParseJson = (text: string) => { export const safeParseJson = (text?: string): any | undefined => {
if (!text) { if (!text) {
return; return;
} }
...@@ -365,7 +365,7 @@ export function clearHistory(datasourceId: string) { ...@@ -365,7 +365,7 @@ export function clearHistory(datasourceId: string) {
} }
export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): 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; const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
return newQueryKeys.concat(`${primaryKey}-${index}`); return newQueryKeys.concat(`${primaryKey}-${index}`);
}, []); }, []);
...@@ -381,7 +381,7 @@ export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRa ...@@ -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) { if (value === null) {
return null; return null;
} }
...@@ -442,7 +442,7 @@ export const getValueWithRefId = (value?: any): any => { ...@@ -442,7 +442,7 @@ export const getValueWithRefId = (value?: any): any => {
return undefined; return undefined;
}; };
export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]) => { export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]): DataQueryError | undefined => {
if (!errors) { if (!errors) {
return undefined; return undefined;
} }
...@@ -530,7 +530,7 @@ export const stopQueryState = (querySubscription: Unsubscribable) => { ...@@ -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) { if (!resolution) {
return { interval: '1s', intervalMs: 1000 }; return { interval: '1s', intervalMs: 1000 };
} }
...@@ -542,7 +542,7 @@ export function deduplicateLogRowsById(rows: LogRowModel[]) { ...@@ -542,7 +542,7 @@ export function deduplicateLogRowsById(rows: LogRowModel[]) {
return _.uniqBy(rows, 'uid'); return _.uniqBy(rows, 'uid');
} }
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => { export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]): DataQueryError | undefined => {
const refId = getValueWithRefId(queryErrors); 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) => { ...@@ -88,7 +88,7 @@ export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data); 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) return Object.keys(data)
.map(key => { .map(key => {
const value = data[key]; const value = data[key];
......
import _ from 'lodash'; import _ from 'lodash';
import { DataQuery } from '@grafana/data'; import { DataQuery } from '@grafana/data';
export const getNextRefIdChar = (queries: DataQuery[]): string => { export const getNextRefIdChar = (queries: DataQuery[]): string | undefined => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => { return _.find(letters, refId => {
......
import { DataSourcePluginMeta, PluginType } from '@grafana/data'; import { DataSourcePluginMeta, PluginType } from '@grafana/data';
import { DataSourcePluginCategory } from 'app/types'; import { DataSourcePluginCategory } from 'app/types';
import { config } from '@grafana/runtime';
export function buildCategories(plugins: DataSourcePluginMeta[]): DataSourcePluginCategory[] { export function buildCategories(plugins: DataSourcePluginMeta[]): DataSourcePluginCategory[] {
const categories: DataSourcePluginCategory[] = [ const categories: DataSourcePluginCategory[] = [
{ id: 'tsdb', title: 'Time series databases', plugins: [] }, { id: 'tsdb', title: 'Time series databases', plugins: [] },
{ id: 'logging', title: 'Logging & document 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: 'sql', title: 'SQL', plugins: [] },
{ id: 'cloud', title: 'Cloud', plugins: [] }, { id: 'cloud', title: 'Cloud', plugins: [] },
{ id: 'enterprise', title: 'Enterprise plugins', plugins: [] }, { id: 'enterprise', title: 'Enterprise plugins', plugins: [] },
{ id: 'other', title: 'Others', plugins: [] }, { id: 'other', title: 'Others', plugins: [] },
]; ].filter(item => item);
const categoryIndex: Record<string, DataSourcePluginCategory> = {}; const categoryIndex: Record<string, DataSourcePluginCategory> = {};
const pluginIndex: Record<string, DataSourcePluginMeta> = {}; const pluginIndex: Record<string, DataSourcePluginMeta> = {};
...@@ -66,6 +68,7 @@ function sortPlugins(plugins: DataSourcePluginMeta[]) { ...@@ -66,6 +68,7 @@ function sortPlugins(plugins: DataSourcePluginMeta[]) {
graphite: 95, graphite: 95,
loki: 90, loki: 90,
mysql: 80, mysql: 80,
jaeger: 100,
postgres: 79, postgres: 79,
gcloud: -1, gcloud: -1,
}; };
......
...@@ -148,13 +148,12 @@ describe('Explore', () => { ...@@ -148,13 +148,12 @@ describe('Explore', () => {
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => { it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors(true); const queryErrors = setupErrors(true);
const queryError = getFirstNonQueryRowSpecificError(queryErrors); 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 () => { it('should not filter out a generic error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors(); const queryErrors = setupErrors();
const queryError = getFirstNonQueryRowSpecificError(queryErrors); const queryError = getFirstNonQueryRowSpecificError(queryErrors);
expect(queryError).not.toBeNull();
expect(queryError).toEqual({ expect(queryError).toEqual({
message: 'Error message', message: 'Error message',
status: '400', status: '400',
......
...@@ -20,33 +20,33 @@ import { ...@@ -20,33 +20,33 @@ import {
changeSize, changeSize,
initializeExplore, initializeExplore,
modifyQueries, modifyQueries,
refreshExplore,
scanStart, scanStart,
setQueries, setQueries,
refreshExplore,
updateTimeRange,
toggleGraph, toggleGraph,
addQueryRow, addQueryRow,
updateTimeRange,
} from './state/actions'; } from './state/actions';
// Types // Types
import { import {
AbsoluteTimeRange,
DataQuery, DataQuery,
DataSourceApi, DataSourceApi,
GraphSeriesXY,
PanelData, PanelData,
RawTimeRange, RawTimeRange,
TimeRange, TimeRange,
GraphSeriesXY,
TimeZone, TimeZone,
AbsoluteTimeRange,
LoadingState, LoadingState,
ExploreMode, ExploreMode,
} from '@grafana/data'; } 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 { StoreState } from 'app/types';
import { import {
ensureQueries,
DEFAULT_RANGE, DEFAULT_RANGE,
DEFAULT_UI_STATE, DEFAULT_UI_STATE,
ensureQueries,
getTimeRangeFromUrl, getTimeRangeFromUrl,
getTimeRange, getTimeRange,
lastUsedDatasourceKeyForOrgId, lastUsedDatasourceKeyForOrgId,
...@@ -70,6 +70,18 @@ const getStyles = stylesFactory(() => { ...@@ -70,6 +70,18 @@ const getStyles = stylesFactory(() => {
button: css` button: css`
margin: 1em 4px 0 0; 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> { ...@@ -328,14 +340,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
</button> </button>
</div> </div>
<ErrorContainer queryError={queryError} /> <ErrorContainer queryError={queryError} />
<AutoSizer onResize={this.onResize} disableHeight> <AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
{({ width }) => { {({ width }) => {
if (width === 0) { if (width === 0) {
return null; return null;
} }
return ( return (
<main className={`m-t-2 ${styles.logsMain}`} style={{ width }}> <main className={cx('m-t-2', styles.logsMain, styles.fullHeight)} style={{ width }}>
<ErrorBoundaryAlert> <ErrorBoundaryAlert>
{showStartPage && StartPage && ( {showStartPage && StartPage && (
<div className={'grafana-info-box grafana-info-box--max-lg'}> <div className={'grafana-info-box grafana-info-box--max-lg'}>
...@@ -379,6 +391,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -379,6 +391,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onStopScanning={this.onStopScanning} 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 && ( {showRichHistory && (
...@@ -448,7 +472,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia ...@@ -448,7 +472,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
newMode = supportedModes[0]; newMode = supportedModes[0];
} }
} else { } 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; const initialUI = ui || DEFAULT_UI_STATE;
......
...@@ -366,7 +366,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps ...@@ -366,7 +366,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
containerWidth, containerWidth,
} = exploreItem; } = exploreItem;
const hasLiveOption = datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs; const hasLiveOption = !!(datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs);
return { return {
datasourceMissing, datasourceMissing,
......
...@@ -14,12 +14,13 @@ import { ...@@ -14,12 +14,13 @@ import {
TimeRange, TimeRange,
LogsMetaItem, LogsMetaItem,
GraphSeriesXY, GraphSeriesXY,
Field,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types'; 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 { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors'; import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors';
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
...@@ -57,6 +58,7 @@ interface LogsContainerProps { ...@@ -57,6 +58,7 @@ interface LogsContainerProps {
syncedTimes: boolean; syncedTimes: boolean;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
isPaused: boolean; isPaused: boolean;
splitOpen: typeof splitOpen;
} }
export class LogsContainer extends PureComponent<LogsContainerProps> { export class LogsContainer extends PureComponent<LogsContainerProps> {
...@@ -87,6 +89,30 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -87,6 +89,30 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
return []; 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() { render() {
const { const {
loading, loading,
...@@ -149,7 +175,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -149,7 +175,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
scanRange={range.raw} scanRange={range.raw}
width={width} width={width}
getRowContext={this.getLogRowContext} getRowContext={this.getLogRowContext}
getFieldLinks={getLinksFromLogsField} getFieldLinks={this.getFieldLinks}
/> />
</Collapse> </Collapse>
</LogsCrossFadeTransition> </LogsCrossFadeTransition>
...@@ -199,6 +225,7 @@ const mapDispatchToProps = { ...@@ -199,6 +225,7 @@ const mapDispatchToProps = {
changeDedupStrategy, changeDedupStrategy,
toggleLogLevelAction, toggleLogLevelAction,
updateTimeRange, updateTimeRange,
splitOpen,
}; };
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
...@@ -46,7 +46,7 @@ export class TableContainer extends PureComponent<TableContainerProps> { ...@@ -46,7 +46,7 @@ export class TableContainer extends PureComponent<TableContainerProps> {
return ( return (
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}> <Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
{hasTableResult ? ( {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' }]} /> <MetaInfoText metaItems={[{ value: '0 series returned' }]} />
)} )}
......
...@@ -5,9 +5,9 @@ import { connect } from 'react-redux'; ...@@ -5,9 +5,9 @@ import { connect } from 'react-redux';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import Explore from './Explore';
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui'; import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
import { resetExploreAction } from './state/actionTypes'; import { resetExploreAction } from './state/actionTypes';
import Explore from './Explore';
interface WrapperProps { interface WrapperProps {
split: boolean; split: boolean;
...@@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> {
return ( return (
<div className="page-scrollbar-wrapper"> <div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page"> <CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page">
<div className="explore-wrapper"> <div style={{ height: '100%' }} className="explore-wrapper">
<ErrorBoundaryAlert style="page"> <ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.left} /> <Explore exploreId={ExploreId.left} />
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
......
...@@ -123,7 +123,7 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo ...@@ -123,7 +123,7 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
*/ */
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> { export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
let newDataSourceInstance: DataSourceApi = null; let newDataSourceInstance: DataSourceApi;
if (!datasource) { if (!datasource) {
newDataSourceInstance = await getDatasourceSrv().get(); newDataSourceInstance = await getDatasourceSrv().get();
...@@ -317,7 +317,7 @@ export const loadDatasourceReady = ( ...@@ -317,7 +317,7 @@ export const loadDatasourceReady = (
instance: DataSourceApi, instance: DataSourceApi,
orgId: number orgId: number
): PayloadAction<LoadDatasourceReadyPayload> => { ): PayloadAction<LoadDatasourceReadyPayload> => {
const historyKey = `grafana.explore.history.${instance.meta.id}`; const historyKey = `grafana.explore.history.${instance.meta?.id}`;
const history = store.getObject(historyKey, []); const history = store.getObject(historyKey, []);
// Save last-used datasource // Save last-used datasource
...@@ -340,7 +340,7 @@ export const loadDatasourceReady = ( ...@@ -340,7 +340,7 @@ export const loadDatasourceReady = (
export const importQueries = ( export const importQueries = (
exploreId: ExploreId, exploreId: ExploreId,
queries: DataQuery[], queries: DataQuery[],
sourceDataSource: DataSourceApi, sourceDataSource: DataSourceApi | undefined,
targetDataSource: DataSourceApi targetDataSource: DataSourceApi
): ThunkResult<void> => { ): ThunkResult<void> => {
return async dispatch => { return async dispatch => {
...@@ -352,7 +352,7 @@ export const importQueries = ( ...@@ -352,7 +352,7 @@ export const importQueries = (
let importedQueries = queries; let importedQueries = queries;
// Check if queries can be imported from previously selected datasource // 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 // Keep same queries if same type of datasource
importedQueries = [...queries]; importedQueries = [...queries];
} else if (targetDataSource.importQueries) { } else if (targetDataSource.importQueries) {
...@@ -701,18 +701,31 @@ export function splitClose(itemId: ExploreId): ThunkResult<void> { ...@@ -701,18 +701,31 @@ export function splitClose(itemId: ExploreId): ThunkResult<void> {
* The right state is automatically initialized. * The right state is automatically initialized.
* The copy keeps all query modifications but wipes the query results. * The copy keeps all query modifications but wipes the query results.
*/ */
export function splitOpen(): ThunkResult<void> { export function splitOpen(dataSourceName?: string, query?: string): ThunkResult<void> {
return (dispatch, getState) => { return async (dispatch, getState) => {
// Clone left state to become the right state // Clone left state to become the right state
const leftState = getState().explore[ExploreId.left]; const leftState: ExploreItemState = getState().explore[ExploreId.left];
const queryState = getState().location.query[ExploreId.left] as string; const rightState: ExploreItemState = {
const urlState = parseUrlState(queryState);
const itemState: ExploreItemState = {
...leftState, ...leftState,
queries: leftState.queries.slice(),
urlState,
}; };
dispatch(splitOpenAction({ itemState })); const queryState = getState().location.query[ExploreId.left] as string;
const urlState = parseUrlState(queryState);
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()); dispatch(stateSave());
}; };
} }
...@@ -757,7 +770,8 @@ const togglePanelActionCreator = ( ...@@ -757,7 +770,8 @@ const togglePanelActionCreator = (
} }
dispatch(actionCreator({ exploreId })); 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) { if (shouldRunQueries) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
......
...@@ -599,6 +599,7 @@ export const updateChildRefreshState = ( ...@@ -599,6 +599,7 @@ export const updateChildRefreshState = (
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => { const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => {
const supportsGraph = dataSource.meta.metrics; const supportsGraph = dataSource.meta.metrics;
const supportsLogs = dataSource.meta.logs; const supportsLogs = dataSource.meta.logs;
const supportsTracing = dataSource.meta.tracing;
let mode = currentMode || ExploreMode.Metrics; let mode = currentMode || ExploreMode.Metrics;
const supportedModes: ExploreMode[] = []; const supportedModes: ExploreMode[] = [];
...@@ -611,13 +612,17 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo ...@@ -611,13 +612,17 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo
supportedModes.push(ExploreMode.Logs); supportedModes.push(ExploreMode.Logs);
} }
if (supportsTracing) {
supportedModes.push(ExploreMode.Tracing);
}
if (supportedModes.length === 1) { if (supportedModes.length === 1) {
mode = supportedModes[0]; mode = supportedModes[0];
} }
// HACK: Used to set Loki's default explore mode to Logs mode. // 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 // 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; mode = ExploreMode.Logs;
} }
......
...@@ -54,8 +54,8 @@ describe('getLinksFromLogsField', () => { ...@@ -54,8 +54,8 @@ describe('getLinksFromLogsField', () => {
}; };
const links = getLinksFromLogsField(field, 2); const links = getLinksFromLogsField(field, 2);
expect(links.length).toBe(2); expect(links.length).toBe(2);
expect(links[0].href).toBe('http://domain.com/3'); expect(links[0].linkModel.href).toBe('http://domain.com/3');
expect(links[1].href).toBe('http://anotherdomain.sk/3'); expect(links[1].linkModel.href).toBe('http://anotherdomain.sk/3');
}); });
it('handles zero links', () => { it('handles zero links', () => {
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
LinkModel, LinkModel,
formattedValueToString, formattedValueToString,
DisplayValue, DisplayValue,
DataLink,
} from '@grafana/data'; } from '@grafana/data';
import { getLinkSrv } from './link_srv'; import { getLinkSrv } from './link_srv';
import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy'; import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy';
...@@ -143,7 +144,10 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane ...@@ -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 = {}; const scopedVars: any = {};
scopedVars['__value'] = { scopedVars['__value'] = {
value: { value: {
...@@ -153,6 +157,11 @@ export const getLinksFromLogsField = (field: Field, rowIndex: number): Array<Lin ...@@ -153,6 +157,11 @@ export const getLinksFromLogsField = (field: Field, rowIndex: number): Array<Lin
}; };
return field.config.links 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 () => ...@@ -13,6 +13,8 @@ const grafanaPlugin = async () =>
const influxdbPlugin = async () => const influxdbPlugin = async () =>
await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module'); await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module');
const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/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 () => const mixedPlugin = async () =>
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module'); await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
const mysqlPlugin = async () => const mysqlPlugin = async () =>
...@@ -64,6 +66,7 @@ const builtInPlugins: any = { ...@@ -64,6 +66,7 @@ const builtInPlugins: any = {
'app/plugins/datasource/grafana/module': grafanaPlugin, 'app/plugins/datasource/grafana/module': grafanaPlugin,
'app/plugins/datasource/influxdb/module': influxdbPlugin, 'app/plugins/datasource/influxdb/module': influxdbPlugin,
'app/plugins/datasource/loki/module': lokiPlugin, 'app/plugins/datasource/loki/module': lokiPlugin,
'app/plugins/datasource/jaeger/module': jaegerPlugin,
'app/plugins/datasource/mixed/module': mixedPlugin, 'app/plugins/datasource/mixed/module': mixedPlugin,
'app/plugins/datasource/mysql/module': mysqlPlugin, 'app/plugins/datasource/mysql/module': mysqlPlugin,
'app/plugins/datasource/postgres/module': postgresPlugin, 'app/plugins/datasource/postgres/module': postgresPlugin,
......
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}
/>
</>
);
};
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;
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),
};
}
}
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);
{
"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'; ...@@ -4,7 +4,7 @@ import cx from 'classnames';
import { FormField } from '@grafana/ui'; import { FormField } from '@grafana/ui';
import { DerivedFieldConfig } from '../types'; import { DerivedFieldConfig } from '../types';
import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers'; import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers';
import { ArrayVector, FieldType } from '@grafana/data'; import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data';
type Props = { type Props = {
derivedFields: DerivedFieldConfig[]; derivedFields: DerivedFieldConfig[];
...@@ -90,7 +90,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string) ...@@ -90,7 +90,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
try { try {
const testMatch = debugText.match(field.matcherRegex); const testMatch = debugText.match(field.matcherRegex);
const value = testMatch && testMatch[1]; const value = testMatch && testMatch[1];
let link; let link: LinkModel<Field>;
if (field.url && value) { if (field.url && value) {
link = getLinksFromLogsField( link = getLinksFromLogsField(
...@@ -103,7 +103,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string) ...@@ -103,7 +103,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
}, },
}, },
0 0
)[0]; )[0].linkModel;
} }
return { return {
......
import React from 'react'; import React, { useState } from 'react';
import { css } from 'emotion'; 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 { VariableSuggestion } from '@grafana/data';
import { DataSourceSelectItem } from '@grafana/data';
import { DerivedFieldConfig } from '../types'; 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(() => ({ const getStyles = stylesFactory(() => ({
firstRow: css` row: css`
display: flex; display: flex;
align-items: baseline; align-items: baseline;
`, `,
...@@ -27,6 +32,7 @@ type Props = { ...@@ -27,6 +32,7 @@ type Props = {
export const DerivedField = (props: Props) => { export const DerivedField = (props: Props) => {
const { value, onChange, onDelete, suggestions, className } = props; const { value, onChange, onDelete, suggestions, className } = props;
const styles = getStyles(); const styles = getStyles();
const [hasIntenalLink, setHasInternalLink] = useState(!!value.datasourceName);
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
onChange({ onChange({
...@@ -37,7 +43,7 @@ export const DerivedField = (props: Props) => { ...@@ -37,7 +43,7 @@ export const DerivedField = (props: Props) => {
return ( return (
<div className={className}> <div className={className}>
<div className={styles.firstRow}> <div className={styles.row}>
<FormField <FormField
className={styles.nameField} className={styles.nameField}
labelWidth={5} labelWidth={5}
...@@ -93,6 +99,64 @@ export const DerivedField = (props: Props) => { ...@@ -93,6 +99,64 @@ export const DerivedField = (props: Props) => {
width: 100%; 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> </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 { ...@@ -51,6 +51,7 @@ import {
} from './types'; } from './types';
import { LegacyTarget, LiveStreams } from './live_streams'; import { LegacyTarget, LiveStreams } from './live_streams';
import LanguageProvider from './language_provider'; import LanguageProvider from './language_provider';
import { serializeParams } from '../../../core/utils/fetch';
export type RangeQueryOptions = Pick<DataQueryRequest<LokiQuery>, 'range' | 'intervalMs' | 'maxDataPoints' | 'reverse'>; export type RangeQueryOptions = Pick<DataQueryRequest<LokiQuery>, 'range' | 'intervalMs' | 'maxDataPoints' | 'reverse'>;
export const DEFAULT_MAX_LINES = 1000; export const DEFAULT_MAX_LINES = 1000;
...@@ -68,12 +69,6 @@ const DEFAULT_QUERY_PARAMS: Partial<LokiLegacyQueryRequest> = { ...@@ -68,12 +69,6 @@ const DEFAULT_QUERY_PARAMS: Partial<LokiLegacyQueryRequest> = {
query: '', query: '',
}; };
function serializeParams(data: Record<string, any>) {
return Object.keys(data)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
.join('&');
}
interface LokiContextQueryOptions { interface LokiContextQueryOptions {
direction?: 'BACKWARD' | 'FORWARD'; direction?: 'BACKWARD' | 'FORWARD';
limit?: number; limit?: number;
......
...@@ -395,11 +395,16 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul ...@@ -395,11 +395,16 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul
const fields = derivedFields.reduce((acc, field) => { const fields = derivedFields.reduce((acc, field) => {
const config: FieldConfig = {}; const config: FieldConfig = {};
if (field.url) { if (field.url || field.datasourceName) {
config.links = [ config.links = [
{ {
url: field.url, url: field.url,
title: '', title: '',
meta: field.datasourceName
? {
datasourceName: field.datasourceName,
}
: undefined,
}, },
]; ];
} }
......
...@@ -127,6 +127,7 @@ export type DerivedFieldConfig = { ...@@ -127,6 +127,7 @@ export type DerivedFieldConfig = {
matcherRegex: string; matcherRegex: string;
name: string; name: string;
url?: string; url?: string;
datasourceName?: string;
}; };
export interface TransformerOptions { export interface TransformerOptions {
......
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}
/>
);
};
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>
);
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;
}
}
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);
{
"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"
}
]
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment