Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instrument vis_type_vislib, lens and vis_type_timeseries with execution context service #105206

Merged
merged 28 commits into from
Jul 19, 2021

Conversation

mshustov
Copy link
Contributor

@mshustov mshustov commented Jul 12, 2021

Summary

Part of #101587
It integrates a few visualizations with a mechanism allowing users to understand what Kibana entity initiated a search request.
It can be used to identify a source of slowness via Elasticsearch slow log query: the slow log record contains x-opaque-id header, which value is propagated from the Kibana server or browser App.

The current PR integrates the execution_context service with :

  • vis_type_vislib
    export const VislibChartType = Object.freeze({
    Histogram: 'histogram' as const,
    HorizontalBar: 'horizontal_bar' as const,
    Line: 'line' as const,
    Pie: 'pie' as const,
    Area: 'area' as const,
    PointSeries: 'point_series' as const,
    Heatmap: 'heatmap' as const,
    Gauge: 'gauge' as const,
    Goal: 'goal' as const,
    Metric: 'metric' as const,
    });
  • lens
  • vis_type_timeseries

I'd appreciate any suggestion from the team-owners on improving the logic or general guidance on the execution_context service integration.

Implementation

The browser environment doesn't provide an API to keep track of the "thread" context during async operations.
In nodejs env, we rely on AsyncLocalStorage and Async hooks for this purpose.
Thus, for the browser env, we have to implement an alternative approach. A Kibana plugin creates an async context object and passes it manually through the whole invocation chain to a place when the Kibana browser app communicates with the Kibana server via an HTTP protocol. Then the propagated async context object is serialized and passed through the network to the Kibana server. In turn, the Kibana server stores it as a part of the "thread" context, emits async context object to the logs, and attaches id and type of async context object as part of x-opaque-id header to every call to the Elasticsearch server. Therefore, a Kibana user can identify what a feature initiated a request via Kibana logs (under elasticsearch.query logger) or Elasticsearch logs (x-opaque-id is attached to search slow logs).

How to test

Unfortunately, right at this moment, only manually. I'm going to add integration tests in a follow-up PR.
Steps to test the PR manually:

  • setup logging to observe request.id: opaqueId in elasticsearch response and execution_context whenever it's set
logging:
  appenders:
    myconsole:
      type: console
      layout:
        type: pattern
        pattern: '%date|%meta'
  loggers:
    - name: elasticsearch.query
      level: all
      appenders: [myconsole]
    - name: elasticsearch.query.taskManager
      level: off
    - name: execution_context
      level: debug
      appenders: [console]
  • run Kibana
  • navigate to Kibana Browser App
  • Install sample data. I tested on Sample eCommerce orders.
  • Navigate to dashboards.
  • You should be able to see the output like this:
[2021-07-12T13:14:28.944+03:00][DEBUG][execution_context] stored the execution context: {"requestId":"d568e29f-c3de-4ef6-a311-225b1358f8b7","type":"visuzalization","name":"gauge","description":"[eCommerce] Average Sales Price","id":"4b3ec120-b892-11e8-a6d9-e546fe2bba5f","url":"/bon/app/visualize#/edit/4b3ec120-b892-11e8-a6d9-e546fe2bba5f"}

2021-07-12T13:14:28.965+03:00|{"http":{"request":{"id":"d568e29f-c3de-4ef6-a311-225b1358f8b7;kibana:visuzalization:gauge:4b3ec120-b892-11e8-a6d9-e546fe2bba5f"}}}

@mshustov mshustov added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v7.15.0 labels Jul 12, 2021
@@ -146,6 +153,7 @@ describe('configureClient', () => {
"200
GET /foo?hello=dolly
{\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}",
undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to update the test instead of refactoring.

? {
http: { request: { id: event.meta.request.options.opaqueId } },
}
: undefined; // do not clutter logs if opaqueId is not present
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"request": { } adds noise to the logs and consumes more disk space, but doesn't bring any value.

const opaqueId = event.meta.request.options.opaqueId;
const meta = opaqueId
? {
http: { request: { id: event.meta.request.options.opaqueId } },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will remove this logic when we start including trace.id in the logs. #102699
Instead, trace.id will be used for the log correlation. But right now we need it for testing.

export interface KibanaExecutionContext {
// to make it compatible with SerializableState
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type KibanaExecutionContext = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to get is incompatible with index signature error when declared it as interface.
We cannot use KibanaExecutionContext extends SerializableState since Core cannot depend on the Kibana plugins.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is the relevant typescript issue: microsoft/TypeScript#15300

@@ -58,6 +62,7 @@ export function getFunctionDefinition({
timeFields: args.timeFields,
timeRange: get(input, 'timeRange', undefined),
getNow,
executionContext: executionContext?.toJSON(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data plugin data fetching model is a bit different from the one most plugins use. It doesn't send an HTTP request for every search operation, but batches them according to some internal rules and send them in bulk. In turn, the Kibana server parses the batch and issues a dedicated search request for every search operation. Kibana server streams Elasticsearch server response back to the browser as soon as every search operation is finished.
Untitled-2021-07-05-1246

So I had to refactor the plugin a bit to support executionContext propagation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we passing json here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the downstream code requires SerializableState, so I had to convert to json before calling fetch$

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SerializableState is any object that can be serialized, seems KibanaExecutionContext already is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to make it serializable without converting to json string ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to make it serializable without converting to json string ?

It's not a json string, it returns a json object. Actually, any object with toJSON method is serializable with JSON.stringify, but SerializableState interface doesn't support it.

executionContext: this.deps.start().core.executionContext.create({
type: this.vis.params.type,
name: this.vis.type.name,
id: this.vis.id ?? 'unknown_id',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what cases it may be empty? What should we use instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsaved visualization

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

embeddable shouldn't be creating the execution context (as else we don't know if request came from dashboard or visualize for example) it should be the app that is responsible for this.

we should add execution context to EmbeddableInput (just as we did with searchSessionId)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed in person, we will add this later. see Nested context in #102629

name: this.savedVis.visualizationType ?? '',
description: this.savedVis.title ?? this.savedVis.description ?? '',
id: this.id,
url: this.output.editUrl,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw url: "app/lens#/edit_by_value" during the testing. What url should I use instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's probably correct url, other state is passed thru navigateToAppwithstate

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the "by-value" Lens panels we can't provide a linkable URL yet. Is there a way we could link to the dashboard URL instead, as the dashboard is has a URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we could link to the dashboard URL instead, as the dashboard is has a URL?

not yet. in #102626 I'm going to add nested context support, so we can create and link dashboard context and visualization context.

@mshustov mshustov marked this pull request as ready for review July 12, 2021 12:58
@mshustov mshustov requested a review from a team July 12, 2021 12:58
@mshustov mshustov requested review from a team as code owners July 12, 2021 12:58
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

@ppisljar
Copy link
Member

ignore my comment about using search sessions for this i don't think its gonna be helpful

@mshustov mshustov marked this pull request as ready for review July 14, 2021 14:25
@mshustov mshustov requested a review from ppisljar July 14, 2021 14:32
Copy link
Contributor

@wylieconlon wylieconlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested according to your instructions and found that it was working well. My main comments are about the actual values that you've chosen for Lens & Visualize executionContext.

| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | <code>string</code> | unique value to indentify find the source |
| [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) | <code>string</code> | public name of a user-facing feature |
| [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) | <code>string</code> | Kibana application initated an operation. Can be narrowed to an enum later. |
| [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) | <code>string</code> | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mshustov It looks like the generated docs have lost some comments- can you bring them back?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm..it's how our doc generator handles interface conversion into type. Let me see what I can do with it.

Comment on lines 395 to 396
type: this.vis.params.type,
name: this.vis.type.name,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these values are the same in practice, is that intentional? Seems like if they are going to be the same value they should use the same accessor.

Copy link
Contributor Author

@mshustov mshustov Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to use type: visualization, it might be useful for cases like lens or actions when we need an additional sub-type to identify a source.

type: this.vis.params.type,
name: this.vis.type.name,
id: this.vis.id ?? 'unknown_id',
description: this.vis.title ?? this.vis.type.title,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the title should be used as name and this.vis.description should be used as the description?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is documented as the public name of a user-facing feature so I think providing the name of the type of visualization better maps to that field. description is better suited for the specific instance of the feature, like the specific visualization title.

x-pack/plugins/lens/public/embeddable/embeddable.tsx Outdated Show resolved Hide resolved
@@ -323,7 +325,15 @@ export class Embeddable
if (this.input.onLoad) {
this.input.onLoad(true);
}
const executionContext = this.deps.executionContext.create({
type: this.savedVis.type ?? 'lens',
name: this.savedVis.visualizationType ?? '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you want to track it as name: 'lnsPie' for example? It's not a unique name.

Copy link
Contributor Author

@mshustov mshustov Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wylieconlon Yes, id and description might be unique, but name is a public-facing name of the Kibana entity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like maybe we could make this parameter name more explicit in a follow-up. Maybe this would be more clear:

  • type renamed to feature
  • name renamed to featureSubtype
  • description renamed to instanceDescription
  • id renamed to instanceId

@mshustov WDYT?

name: this.savedVis.visualizationType ?? '',
description: this.savedVis.title ?? this.savedVis.description ?? '',
id: this.id,
url: this.output.editUrl,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the "by-value" Lens panels we can't provide a linkable URL yet. Is there a way we could link to the dashboard URL instead, as the dashboard is has a URL?


export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types';

export { URL_MAX_LENGTH } from './core_app';

export type { KibanaExecutionContext } from './execution_context';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the additional type export from the same location needed?

@joshdover joshdover self-assigned this Jul 16, 2021
Copy link
Contributor

@joshdover joshdover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one test that I think needs to be adjusted, but otherwise LGTM. I'll push up the change to that test while @mshustov is on PTO. We can merge once we get @ppisljar's ✅

type: this.vis.params.type,
name: this.vis.type.name,
id: this.vis.id ?? 'unknown_id',
description: this.vis.title ?? this.vis.type.title,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is documented as the public name of a user-facing feature so I think providing the name of the type of visualization better maps to that field. description is better suited for the specific instance of the feature, like the specific visualization title.

Copy link
Member

@ppisljar ppisljar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code LGTM

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
core 1071 1072 +1
data 3278 3281 +3
expressions 1565 1567 +2
total +6

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
lens 1.5MB 1.5MB +557.0B
visualizations 101.1KB 101.4KB +373.0B
total +930.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
core 421.6KB 421.7KB +96.0B
data 843.2KB 843.6KB +380.0B
expressions 213.8KB 213.9KB +152.0B
lens 28.8KB 28.9KB +112.0B
visTypeTimeseries 24.5KB 24.7KB +138.0B
total +878.0B
Unknown metric groups

API count

id before after diff
core 2328 2329 +1
data 3844 3847 +3
expressions 1998 2003 +5
total +9

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @joshdover

@joshdover joshdover added the auto-backport Deprecated - use backport:version if exact versions are needed label Jul 19, 2021
@joshdover joshdover merged commit f9089e1 into elastic:master Jul 19, 2021
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Jul 19, 2021
…on context service (elastic#105206)

Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
@kibanamachine
Copy link
Contributor

💚 Backport successful

Status Branch Result
7.x

This backport PR will be merged automatically after passing CI.

kibanamachine added a commit that referenced this pull request Jul 19, 2021
…on context service (#105206) (#106117)

Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>

Co-authored-by: Mikhail Shustov <restrry@gmail.com>
Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
@mshustov mshustov deleted the inspect-vis branch July 29, 2021 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v7.15.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants