mirror of https://github.com/grafana/grafana.git
				
				
				
			Elasticsearch: View in context feature for logs (#28764)
* Elasticsearch: View in context feature for logs * Fixing unused type * Limit show context to esVersion > 5 * Fixing scope for showContextToggle; removing console.log * Fix typing; adding check for lineField * Update test to reflect new sorting keys for logs * Removing sort from metadata fields * Adding comment for clarity * Fixing scenerio where the data is missing * remove export & use optional chaining Co-authored-by: Elfo404 <gio.ricci@grafana.com>
This commit is contained in:
		
							parent
							
								
									8b21290164
								
							
						
					
					
						commit
						491a4dc967
					
				|  | @ -91,6 +91,16 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { | |||
|     return []; | ||||
|   }; | ||||
| 
 | ||||
|   showContextToggle = (): boolean => { | ||||
|     const { datasourceInstance } = this.props; | ||||
| 
 | ||||
|     if (datasourceInstance?.showContextToggle) { | ||||
|       return datasourceInstance.showContextToggle(); | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   }; | ||||
| 
 | ||||
|   getFieldLinks = (field: Field, rowIndex: number) => { | ||||
|     return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range); | ||||
|   }; | ||||
|  | @ -157,7 +167,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { | |||
|               timeZone={timeZone} | ||||
|               scanning={scanning} | ||||
|               scanRange={range.raw} | ||||
|               showContextToggle={this.props.datasourceInstance?.showContextToggle} | ||||
|               showContextToggle={this.showContextToggle} | ||||
|               width={width} | ||||
|               getRowContext={this.getLogRowContext} | ||||
|               getFieldLinks={this.getFieldLinks} | ||||
|  |  | |||
|  | @ -9,6 +9,8 @@ import { | |||
|   DataLink, | ||||
|   PluginMeta, | ||||
|   DataQuery, | ||||
|   LogRowModel, | ||||
|   Field, | ||||
|   MetricFindValue, | ||||
| } from '@grafana/data'; | ||||
| import LanguageProvider from './language_provider'; | ||||
|  | @ -21,6 +23,7 @@ import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; | |||
| import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; | ||||
| import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; | ||||
| import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types'; | ||||
| import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; | ||||
| import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; | ||||
| import { | ||||
|   isMetricAggregationWithField, | ||||
|  | @ -432,6 +435,74 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic | |||
|     return text; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * This method checks to ensure the user is running a 5.0+ cluster. This is | ||||
|    * necessary bacause the query being used for the getLogRowContext relies on the | ||||
|    * search_after feature. | ||||
|    */ | ||||
|   showContextToggle(): boolean { | ||||
|     return this.esVersion > 5; | ||||
|   } | ||||
| 
 | ||||
|   getLogRowContext = async (row: LogRowModel, options?: RowContextOptions): Promise<{ data: DataFrame[] }> => { | ||||
|     const sortField = row.dataFrame.fields.find(f => f.name === 'sort'); | ||||
|     const searchAfter = sortField?.values.get(row.rowIndex) || [row.timeEpochMs]; | ||||
|     const range = this.timeSrv.timeRange(); | ||||
|     const direction = options?.direction === 'FORWARD' ? 'asc' : 'desc'; | ||||
|     const header = this.getQueryHeader('query_then_fetch', range.from, range.to); | ||||
|     const limit = options?.limit ?? 10; | ||||
|     const esQuery = JSON.stringify({ | ||||
|       size: limit, | ||||
|       query: { | ||||
|         bool: { | ||||
|           filter: [ | ||||
|             { | ||||
|               range: { | ||||
|                 [this.timeField]: { | ||||
|                   gte: range.from.valueOf(), | ||||
|                   lte: range.to.valueOf(), | ||||
|                   format: 'epoch_millis', | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|       sort: [{ [this.timeField]: direction }, { _doc: direction }], | ||||
|       search_after: searchAfter, | ||||
|     }); | ||||
|     const payload = [header, esQuery].join('\n') + '\n'; | ||||
|     const url = this.getMultiSearchUrl(); | ||||
|     const response = await this.post(url, payload); | ||||
|     const targets: ElasticsearchQuery[] = [{ refId: `${row.dataFrame.refId}`, metrics: [], isLogsQuery: true }]; | ||||
|     const elasticResponse = new ElasticResponse(targets, transformHitsBasedOnDirection(response, direction)); | ||||
|     const logResponse = elasticResponse.getLogs(this.logMessageField, this.logLevelField); | ||||
|     const dataFrame = _.first(logResponse.data); | ||||
|     if (!dataFrame) { | ||||
|       return { data: [] }; | ||||
|     } | ||||
|     /** | ||||
|      * The LogRowContextProvider requires there is a field in the dataFrame.fields | ||||
|      * named `ts` for timestamp and `line` for the actual log line to display. | ||||
|      * Unfortunatly these fields are hardcoded and are required for the lines to | ||||
|      * be properly displayed. This code just copies the fields based on this.timeField | ||||
|      * and this.logMessageField and recreates the dataFrame so it works. | ||||
|      */ | ||||
|     const timestampField = dataFrame.fields.find((f: Field) => f.name === this.timeField); | ||||
|     const lineField = dataFrame.fields.find((f: Field) => f.name === this.logMessageField); | ||||
|     if (timestampField && lineField) { | ||||
|       return { | ||||
|         data: [ | ||||
|           { | ||||
|             ...dataFrame, | ||||
|             fields: [...dataFrame.fields, { ...timestampField, name: 'ts' }, { ...lineField, name: 'line' }], | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|     } | ||||
|     return logResponse; | ||||
|   }; | ||||
| 
 | ||||
|   query(options: DataQueryRequest<ElasticsearchQuery>): Promise<DataQueryResponse> { | ||||
|     let payload = ''; | ||||
|     const targets = this.interpolateVariablesInQueries(_.cloneDeep(options.targets), options.scopedVars); | ||||
|  | @ -758,3 +829,22 @@ export function enhanceDataFrame(dataFrame: DataFrame, dataLinks: DataLinkConfig | |||
|     field.config.links = [...(field.config.links || []), link]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function transformHitsBasedOnDirection(response: any, direction: 'asc' | 'desc') { | ||||
|   if (direction === 'desc') { | ||||
|     return response; | ||||
|   } | ||||
|   const actualResponse = response.responses[0]; | ||||
|   return { | ||||
|     ...response, | ||||
|     responses: [ | ||||
|       { | ||||
|         ...actualResponse, | ||||
|         hits: { | ||||
|           ...actualResponse.hits, | ||||
|           hits: actualResponse.hits.hits.reverse(), | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -372,6 +372,7 @@ export class ElasticResponse { | |||
|         _id: hit._id, | ||||
|         _type: hit._type, | ||||
|         _index: hit._index, | ||||
|         sort: hit.sort, | ||||
|       }; | ||||
| 
 | ||||
|       if (hit._source) { | ||||
|  | @ -552,6 +553,7 @@ type Doc = { | |||
|   _type: string; | ||||
|   _index: string; | ||||
|   _source?: any; | ||||
|   sort?: Array<string | number>; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | @ -572,6 +574,7 @@ const flattenHits = (hits: Doc[]): { docs: Array<Record<string, any>>; propNames | |||
|       _id: hit._id, | ||||
|       _type: hit._type, | ||||
|       _index: hit._index, | ||||
|       sort: hit.sort, | ||||
|       _source: { ...flattened }, | ||||
|       ...flattened, | ||||
|     }; | ||||
|  |  | |||
|  | @ -131,8 +131,14 @@ export class ElasticQueryBuilder { | |||
| 
 | ||||
|   documentQuery(query: any, size: number) { | ||||
|     query.size = size; | ||||
|     query.sort = {}; | ||||
|     query.sort[this.timeField] = { order: 'desc', unmapped_type: 'boolean' }; | ||||
|     query.sort = [ | ||||
|       { | ||||
|         [this.timeField]: { order: 'desc', unmapped_type: 'boolean' }, | ||||
|       }, | ||||
|       { | ||||
|         _doc: { order: 'desc' }, | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     // fields field not supported on ES 5.x
 | ||||
|     if (this.esVersion < 5) { | ||||
|  |  | |||
|  | @ -243,12 +243,7 @@ describe('ElasticQueryBuilder', () => { | |||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|           sort: { | ||||
|             '@timestamp': { | ||||
|               order: 'desc', | ||||
|               unmapped_type: 'boolean', | ||||
|             }, | ||||
|           }, | ||||
|           sort: [{ '@timestamp': { order: 'desc', unmapped_type: 'boolean' } }, { _doc: { order: 'desc' } }], | ||||
|           script_fields: {}, | ||||
|         }); | ||||
|       }); | ||||
|  | @ -573,7 +568,10 @@ describe('ElasticQueryBuilder', () => { | |||
|           }; | ||||
|           expect(query.query).toEqual(expectedQuery); | ||||
| 
 | ||||
|           expect(query.sort).toEqual({ '@timestamp': { order: 'desc', unmapped_type: 'boolean' } }); | ||||
|           expect(query.sort).toEqual([ | ||||
|             { '@timestamp': { order: 'desc', unmapped_type: 'boolean' } }, | ||||
|             { _doc: { order: 'desc' } }, | ||||
|           ]); | ||||
| 
 | ||||
|           const expectedAggs = { | ||||
|             // FIXME: It's pretty weak to include this '1' in the test as it's not part of what we are testing here and
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue