Add execution context service (#102039)

* add execution context service on the server-side

* integrate execution context service into http service

* add integration tests for execution context + http server

* update core code

* update integration tests

* update settings docs

* add execution context test plugin

* add a client-side test

* remove requestId from execution context

* add execution context service for the client side

* expose execution context service to plugins

* add execution context service for the server-side

* update http service

* update elasticsearch service

* move integration tests from http to execution_context service

* integrate in es client

* expose to plugins

* refactor functional tests

* remove x-opaque-id from create_cluster tests

* update test plugin package.json

* fix type errors in the test mocks

* fix elasticsearch service tests

* add escaping to support non-ascii symbols in description field

* improve test coverage

* update docs

* remove unnecessary import

* update docs

* Apply suggestions from code review

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

* address comments

* remove execution context cleanup

* add option to disable execution_context service on the server side

* put x-opaque-id test back

* put tests back

* add header size limitation to the server side as well

* fix integration tests

* address comments

Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
This commit is contained in:
Mikhail Shustov 2021-07-07 17:46:35 +02:00 committed by GitHub
parent 979c6e8031
commit e01e682917
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 2182 additions and 32 deletions

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [CoreStart](./kibana-plugin-core-public.corestart.md) &gt; [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md)
## CoreStart.executionContext property
[ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md)
<b>Signature:</b>
```typescript
executionContext: ExecutionContextServiceStart;
```

View file

@ -20,6 +20,7 @@ export interface CoreStart
| [chrome](./kibana-plugin-core-public.corestart.chrome.md) | <code>ChromeStart</code> | [ChromeStart](./kibana-plugin-core-public.chromestart.md) |
| [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | <code>DeprecationsServiceStart</code> | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) |
| [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | <code>DocLinksStart</code> | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) |
| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | <code>ExecutionContextServiceStart</code> | [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) |
| [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | <code>FatalErrorsStart</code> | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) |
| [http](./kibana-plugin-core-public.corestart.http.md) | <code>HttpStart</code> | [HttpStart](./kibana-plugin-core-public.httpstart.md) |
| [i18n](./kibana-plugin-core-public.corestart.i18n.md) | <code>I18nStart</code> | [I18nStart](./kibana-plugin-core-public.i18nstart.md) |

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) &gt; [create](./kibana-plugin-core-public.executioncontextservicestart.create.md)
## ExecutionContextServiceStart.create property
Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers.
```js
const context = executionContext.create(...);
http.fetch('/endpoint/', { context });
```
<b>Signature:</b>
```typescript
create: (context: KibanaExecutionContext) => IExecutionContextContainer;
```

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md)
## ExecutionContextServiceStart interface
<b>Signature:</b>
```typescript
export interface ExecutionContextServiceStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) | <code>(context: KibanaExecutionContext) =&gt; IExecutionContextContainer</code> | Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers.
```js
const context = executionContext.create(...);
http.fetch('/endpoint/', { context });
```
|

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) &gt; [context](./kibana-plugin-core-public.httpfetchoptions.context.md)
## HttpFetchOptions.context property
<b>Signature:</b>
```typescript
context?: IExecutionContextContainer;
```

View file

@ -18,6 +18,7 @@ export interface HttpFetchOptions extends HttpRequestInit
| --- | --- | --- |
| [asResponse](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | <code>boolean</code> | When <code>true</code> the return type of [HttpHandler](./kibana-plugin-core-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-core-public.httpresponse.md) with detailed request and response information. When <code>false</code>, the return type will just be the parsed response body. Defaults to <code>false</code>. |
| [asSystemRequest](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | <code>boolean</code> | Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to <code>false</code>. |
| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | <code>IExecutionContextContainer</code> | |
| [headers](./kibana-plugin-core-public.httpfetchoptions.headers.md) | <code>HttpHeadersInit</code> | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md)<!-- -->. |
| [prependBasePath](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | <code>boolean</code> | Whether or not the request should automatically prepend the basePath. Defaults to <code>true</code>. |
| [query](./kibana-plugin-core-public.httpfetchoptions.query.md) | <code>HttpFetchQuery</code> | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md)<!-- -->. |

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md)
## IExecutionContextContainer interface
<b>Signature:</b>
```typescript
export interface IExecutionContextContainer
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) | <code>() =&gt; Record&lt;string, string&gt;</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) &gt; [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md)
## IExecutionContextContainer.toHeader property
<b>Signature:</b>
```typescript
toHeader: () => Record<string, string>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) &gt; [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md)
## KibanaExecutionContext.description property
human readable description. For example, a vis title, action name
<b>Signature:</b>
```typescript
readonly description: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) &gt; [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md)
## KibanaExecutionContext.id property
unique value to indentify find the source
<b>Signature:</b>
```typescript
readonly id: string;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md)
## KibanaExecutionContext interface
<b>Signature:</b>
```typescript
export interface KibanaExecutionContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) | <code>string</code> | human readable description. For example, a vis title, action name |
| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | <code>string</code> | unique value to indentify find the source |
| [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) | <code>string</code> | public name of a user-facing feature |
| [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) | <code>string</code> | Kibana application initated an operation. Can be narrowed to an enum later. |
| [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) | <code>string</code> | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) &gt; [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md)
## KibanaExecutionContext.name property
public name of a user-facing feature
<b>Signature:</b>
```typescript
readonly name: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) &gt; [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md)
## KibanaExecutionContext.type property
Kibana application initated an operation. Can be narrowed to an enum later.
<b>Signature:</b>
```typescript
readonly type: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) &gt; [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md)
## KibanaExecutionContext.url property
in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url
<b>Signature:</b>
```typescript
readonly url?: string;
```

View file

@ -63,6 +63,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | |
| [DomainDeprecationDetails](./kibana-plugin-core-public.domaindeprecationdetails.md) | |
| [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. |
| [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) | |
| [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the <code>message</code> and <code>stack</code> of a fatal Error |
| [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-core-public.httphandler.md)<!-- -->. |
@ -79,12 +80,14 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [I18nStart](./kibana-plugin-core-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
| [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
| [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) | |
| [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. |
| [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. |
| [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | |
| [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md)<!-- -->. |
| [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
| [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) |
| [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) | |
| [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) |
| [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CoreSetup](./kibana-plugin-core-server.coresetup.md) &gt; [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md)
## CoreSetup.executionContext property
[ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md)
<b>Signature:</b>
```typescript
executionContext: ExecutionContextSetup;
```

View file

@ -20,6 +20,7 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
| [context](./kibana-plugin-core-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) |
| [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) | <code>DeprecationsServiceSetup</code> | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) |
| [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) |
| [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md) | <code>ExecutionContextSetup</code> | [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) |
| [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | <code>StartServicesAccessor&lt;TPluginsStart, TStart&gt;</code> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) |
| [http](./kibana-plugin-core-server.coresetup.http.md) | <code>HttpServiceSetup &amp; {</code><br/><code> resources: HttpResources;</code><br/><code> }</code> | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | <code>I18nServiceSetup</code> | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CoreStart](./kibana-plugin-core-server.corestart.md) &gt; [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md)
## CoreStart.executionContext property
[ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md)
<b>Signature:</b>
```typescript
executionContext: ExecutionContextStart;
```

View file

@ -18,6 +18,7 @@ export interface CoreStart
| --- | --- | --- |
| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | <code>CapabilitiesStart</code> | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) |
| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | <code>ElasticsearchServiceStart</code> | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) |
| [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md) | <code>ExecutionContextStart</code> | [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) |
| [http](./kibana-plugin-core-server.corestart.http.md) | <code>HttpServiceStart</code> | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) |
| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | <code>MetricsServiceStart</code> | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) |
| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | <code>SavedObjectsServiceStart</code> | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) |

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) &gt; [get](./kibana-plugin-core-server.executioncontextsetup.get.md)
## ExecutionContextSetup.get() method
Retrieves an opearation meta-data for the current async context.
<b>Signature:</b>
```typescript
get(): IExecutionContextContainer | undefined;
```
<b>Returns:</b>
`IExecutionContextContainer | undefined`

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md)
## ExecutionContextSetup interface
<b>Signature:</b>
```typescript
export interface ExecutionContextSetup
```
## Methods
| Method | Description |
| --- | --- |
| [get()](./kibana-plugin-core-server.executioncontextsetup.get.md) | Retrieves an opearation meta-data for the current async context. |
| [set(context)](./kibana-plugin-core-server.executioncontextsetup.set.md) | Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly. |

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) &gt; [set](./kibana-plugin-core-server.executioncontextsetup.set.md)
## ExecutionContextSetup.set() method
Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly.
<b>Signature:</b>
```typescript
set(context: Partial<KibanaServerExecutionContext>): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| context | <code>Partial&lt;KibanaServerExecutionContext&gt;</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,12 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md)
## ExecutionContextStart type
<b>Signature:</b>
```typescript
export declare type ExecutionContextStart = ExecutionContextSetup;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md)
## IExecutionContextContainer interface
<b>Signature:</b>
```typescript
export interface IExecutionContextContainer
```
## Methods
| Method | Description |
| --- | --- |
| [toJSON()](./kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md) | |
| [toString()](./kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md) | |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md) &gt; [toJSON](./kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md)
## IExecutionContextContainer.toJSON() method
<b>Signature:</b>
```typescript
toJSON(): Readonly<KibanaServerExecutionContext>;
```
<b>Returns:</b>
`Readonly<KibanaServerExecutionContext>`

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md) &gt; [toString](./kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md)
## IExecutionContextContainer.toString() method
<b>Signature:</b>
```typescript
toString(): string;
```
<b>Returns:</b>
`string`

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) &gt; [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md)
## KibanaExecutionContext.description property
human readable description. For example, a vis title, action name
<b>Signature:</b>
```typescript
readonly description: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) &gt; [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md)
## KibanaExecutionContext.id property
unique value to indentify find the source
<b>Signature:</b>
```typescript
readonly id: string;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md)
## KibanaExecutionContext interface
<b>Signature:</b>
```typescript
export interface KibanaExecutionContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) | <code>string</code> | human readable description. For example, a vis title, action name |
| [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 |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) &gt; [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md)
## KibanaExecutionContext.name property
public name of a user-facing feature
<b>Signature:</b>
```typescript
readonly name: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) &gt; [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md)
## KibanaExecutionContext.type property
Kibana application initated an operation. Can be narrowed to an enum later.
<b>Signature:</b>
```typescript
readonly type: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) &gt; [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md)
## KibanaExecutionContext.url property
in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url
<b>Signature:</b>
```typescript
readonly url?: string;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md)
## KibanaServerExecutionContext interface
<b>Signature:</b>
```typescript
export interface KibanaServerExecutionContext extends Partial<KibanaExecutionContext>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) | <code>string</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) &gt; [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md)
## KibanaServerExecutionContext.requestId property
<b>Signature:</b>
```typescript
requestId: string;
```

View file

@ -77,6 +77,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | |
| [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | |
| [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters |
| [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) | |
| [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. |
| [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) | |
| [GetResponse](./kibana-plugin-core-server.getresponse.md) | |
@ -93,6 +94,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. |
| [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) |
| [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md) | |
| [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) | External Url configuration for use in Kibana. |
| [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. |
| [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution |
@ -103,8 +105,10 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | |
| [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional <code>asCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>asInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. |
| [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. |
| [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) | |
| [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. |
| [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. |
| [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) | |
| [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | |
| [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. |
| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @<!-- -->deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 |
@ -248,6 +252,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. |
| [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. |
| [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) |
| [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) | |
| [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. |
| [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by <code>auth</code> interceptor. |
| [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. |

View file

@ -627,7 +627,7 @@ identifies this {kib} instance. *Default: `"your-hostname"`*
setting specifies the port to use. *Default: `5601`*
|[[server-requestId-allowFromAnyIp]] `server.requestId.allowFromAnyIp:`
| Sets whether or not the X-Opaque-Id header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch.
| Sets whether or not the `X-Opaque-Id` header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch.
| `server.requestId.ipAllowlist:`
| A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, <<server-requestId-allowFromAnyIp, `server.requestId.allowFromAnyIp`>> must also be set to `false.`

View file

@ -19,6 +19,7 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { renderingServiceMock } from './rendering/rendering_service.mock';
import { integrationsServiceMock } from './integrations/integrations_service.mock';
import { executionContextServiceMock } from './execution_context/execution_context_service.mock';
import { coreAppMock } from './core_app/core_app.mock';
export const MockInjectedMetadataService = injectedMetadataServiceMock.create();
@ -111,6 +112,14 @@ jest.doMock('./integrations', () => ({
IntegrationsService: IntegrationsServiceConstructor,
}));
export const MockExecutionContextService = executionContextServiceMock.create();
export const ExecutionContextServiceConstructor = jest
.fn()
.mockImplementation(() => MockExecutionContextService);
jest.doMock('./execution_context', () => ({
ExecutionContextService: ExecutionContextServiceConstructor,
}));
export const MockCoreApp = coreAppMock.create();
export const CoreAppConstructor = jest.fn().mockImplementation(() => MockCoreApp);
jest.doMock('./core_app', () => ({

View file

@ -30,6 +30,7 @@ import {
RenderingServiceConstructor,
IntegrationsServiceConstructor,
MockIntegrationsService,
MockExecutionContextService,
CoreAppConstructor,
MockCoreApp,
} from './core_system.test.mocks';
@ -182,6 +183,11 @@ describe('#setup()', () => {
await setupCore();
expect(MockCoreApp.setup).toHaveBeenCalledTimes(1);
});
it('calls executionContext.setup()', async () => {
await setupCore();
expect(MockExecutionContextService.setup).toHaveBeenCalledTimes(1);
});
});
describe('#start()', () => {
@ -269,6 +275,11 @@ describe('#start()', () => {
await startCore();
expect(MockCoreApp.start).toHaveBeenCalledTimes(1);
});
it('calls executionContext.start()', async () => {
await startCore();
expect(MockExecutionContextService.start).toHaveBeenCalledTimes(1);
});
});
describe('#stop()', () => {
@ -327,6 +338,14 @@ describe('#stop()', () => {
expect(MockCoreApp.stop).toHaveBeenCalled();
});
it('calls executionContext.stop()', () => {
const coreSystem = createCoreSystem();
expect(MockExecutionContextService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(MockExecutionContextService.stop).toHaveBeenCalled();
});
it('clears the rootDomElement', async () => {
const rootDomElement = document.createElement('div');
const coreSystem = createCoreSystem({

View file

@ -29,6 +29,7 @@ import { SavedObjectsService } from './saved_objects';
import { IntegrationsService } from './integrations';
import { DeprecationsService } from './deprecations';
import { CoreApp } from './core_app';
import { ExecutionContextService } from './execution_context';
import type { InternalApplicationSetup, InternalApplicationStart } from './application/types';
interface Params {
@ -83,6 +84,7 @@ export class CoreSystem {
private readonly integrations: IntegrationsService;
private readonly coreApp: CoreApp;
private readonly deprecations: DeprecationsService;
private readonly executionContext: ExecutionContextService;
private readonly rootDomElement: HTMLElement;
private readonly coreContext: CoreContext;
private fatalErrorsSetup: FatalErrorsSetup | null = null;
@ -118,6 +120,7 @@ export class CoreSystem {
this.application = new ApplicationService();
this.integrations = new IntegrationsService();
this.deprecations = new DeprecationsService();
this.executionContext = new ExecutionContextService();
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
this.coreApp = new CoreApp(this.coreContext);
@ -137,6 +140,7 @@ export class CoreSystem {
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
this.executionContext.setup();
const application = this.application.setup({ http });
this.coreApp.setup({ application, http, injectedMetadata, notifications });
@ -201,6 +205,7 @@ export class CoreSystem {
notifications,
});
const deprecations = this.deprecations.start({ http });
const executionContext = this.executionContext.start();
this.coreApp.start({ application, docLinks, http, notifications, uiSettings });
@ -217,6 +222,7 @@ export class CoreSystem {
uiSettings,
fatalErrors,
deprecations,
executionContext,
};
await this.plugins.start(core);
@ -260,6 +266,7 @@ export class CoreSystem {
this.i18n.stop();
this.application.stop();
this.deprecations.stop();
this.executionContext.stop();
this.rootDomElement.textContent = '';
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaExecutionContext } from '../../types';
import {
ExecutionContextContainer,
BAGGAGE_MAX_PER_NAME_VALUE_PAIRS,
} from './execution_context_container';
describe('KibanaExecutionContext', () => {
describe('toHeader', () => {
it('returns an escaped string representation of provided execution context', () => {
const context: KibanaExecutionContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'test-descripton',
};
const value = new ExecutionContextContainer(context).toHeader();
expect(value).toMatchInlineSnapshot(`
Object {
"x-kbn-context": "%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22id%22%3A%2242%22%2C%22description%22%3A%22test-descripton%22%7D",
}
`);
});
it('trims a string representation of provided execution context if it is bigger max allowed size', () => {
const context: KibanaExecutionContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'long long test-descripton,'.repeat(1000),
};
const value = new ExecutionContextContainer(context).toHeader();
expect(value).toMatchInlineSnapshot(`
Object {
"x-kbn-context": "%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22id%22%3A%2242%22%2C%22description%22%3A%22long%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test-descripton%2Clong%20long%20test",
}
`);
expect(new Blob(Object.values(value)).size).toBeLessThanOrEqual(
BAGGAGE_MAX_PER_NAME_VALUE_PAIRS
);
});
it('escapes the string representation of provided execution context', () => {
const context: KibanaExecutionContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'описание',
};
const value = new ExecutionContextContainer(context).toHeader();
expect(value).toMatchInlineSnapshot(`
Object {
"x-kbn-context": "%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22id%22%3A%2242%22%2C%22description%22%3A%22%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5%22%7D",
}
`);
});
});
});

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaExecutionContext } from '../../types';
// Switch to the standard Baggage header
// https://github.com/elastic/apm-agent-rum-js/issues/1040
const BAGGAGE_HEADER = 'x-kbn-context';
// Maximum number of bytes per a single name-value pair allowed by w3c spec
// https://w3c.github.io/baggage/
export const BAGGAGE_MAX_PER_NAME_VALUE_PAIRS = 4096;
// a single character can use up to 4 bytes
const MAX_BAGGAGE_LENGTH = BAGGAGE_MAX_PER_NAME_VALUE_PAIRS / 4;
// Limits the header value to max allowed "baggage" header property name-value pair
// It will help us switch to the "baggage" header when it becomes the standard.
// The trimmed value in the logs is better than nothing.
function enforceMaxLength(header: string): string {
return header.slice(0, MAX_BAGGAGE_LENGTH);
}
/**
* @public
*/
export interface IExecutionContextContainer {
toHeader: () => Record<string, string>;
}
export class ExecutionContextContainer implements IExecutionContextContainer {
readonly #context: Readonly<KibanaExecutionContext>;
constructor(context: Readonly<KibanaExecutionContext>) {
this.#context = context;
}
private toString(): string {
const value = JSON.stringify(this.#context);
// escape content as the description property might contain non-ASCII symbols
return enforceMaxLength(encodeURIComponent(value));
}
toHeader() {
return { [BAGGAGE_HEADER]: this.toString() };
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { Plugin } from 'src/core/public';
import type { ExecutionContextServiceStart } from './execution_context_service';
import type { ExecutionContextContainer } from './execution_context_container';
const createContainerMock = () => {
const mock: jest.Mocked<PublicMethodsOf<ExecutionContextContainer>> = {
toHeader: jest.fn(),
};
return mock;
};
const createStartContractMock = () => {
const mock: jest.Mocked<ExecutionContextServiceStart> = {
create: jest.fn().mockReturnValue(createContainerMock()),
};
return mock;
};
const createMock = (): jest.Mocked<Plugin> => ({
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
});
export const executionContextServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
createContainer: createContainerMock,
};

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { CoreService, KibanaExecutionContext } from '../../types';
import {
ExecutionContextContainer,
IExecutionContextContainer,
} from './execution_context_container';
/**
* @public
*/
export interface ExecutionContextServiceStart {
/**
* Creates a context container carrying the meta-data of a runtime operation.
* Provided meta-data will be propagated to Kibana and Elasticsearch servers.
* ```js
* const context = executionContext.create(...);
* http.fetch('/endpoint/', { context });
* ```
*/
create: (context: KibanaExecutionContext) => IExecutionContextContainer;
}
export class ExecutionContextService implements CoreService<void, ExecutionContextServiceStart> {
setup() {}
start(): ExecutionContextServiceStart {
return {
create(context: KibanaExecutionContext) {
return new ExecutionContextContainer(context);
},
};
}
stop() {}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { KibanaExecutionContext } from '../../types';
export { ExecutionContextService } from './execution_context_service';
export type { ExecutionContextServiceStart } from './execution_context_service';
export type { IExecutionContextContainer } from './execution_context_container';

View file

@ -15,6 +15,7 @@ import { first } from 'rxjs/operators';
import { Fetch } from './fetch';
import { BasePath } from './base_path';
import { HttpResponse, HttpFetchOptionsWithPath } from './types';
import { executionContextServiceMock } from '../execution_context/execution_context_service.mock';
function delay<T>(duration: number) {
return new Promise<T>((r) => setTimeout(r, duration));
@ -227,6 +228,19 @@ describe('Fetch', () => {
);
});
it('should inject context headers if provided', async () => {
fetchMock.get('*', {});
const executionContainerMock = executionContextServiceMock.createContainer();
executionContainerMock.toHeader.mockReturnValueOnce({ 'x-kbn-context': 'value' });
await fetchInstance.fetch('/my/path', {
context: executionContainerMock,
});
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'x-kbn-context': 'value',
});
});
// Deprecated header used by legacy platform pre-7.7. Remove in 8.x.
it('should not allow overwriting of kbn-system-api when asSystemRequest: true', async () => {
fetchMock.get('*', {});

View file

@ -124,6 +124,7 @@ export class Fetch {
'Content-Type': 'application/json',
...options.headers,
'kbn-version': this.params.kibanaVersion,
...options.context?.toHeader(),
}),
};

View file

@ -8,6 +8,7 @@
import { Observable } from 'rxjs';
import { MaybePromise } from '@kbn/utility-types';
import type { IExecutionContextContainer } from '../execution_context';
/** @public */
export interface HttpSetup {
@ -270,6 +271,8 @@ export interface HttpFetchOptions extends HttpRequestInit {
* response information. When `false`, the return type will just be the parsed response body. Defaults to `false`.
*/
asResponse?: boolean;
context?: IExecutionContextContainer;
}
/**

View file

@ -65,6 +65,7 @@ import { ApplicationSetup, Capabilities, ApplicationStart } from './application'
import { DocLinksStart } from './doc_links';
import { SavedObjectsStart } from './saved_objects';
import { DeprecationsServiceStart } from './deprecations';
import type { ExecutionContextServiceStart } from './execution_context';
export type {
PackageInfo,
@ -185,6 +186,12 @@ export type {
export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations';
export type {
IExecutionContextContainer,
ExecutionContextServiceStart,
KibanaExecutionContext,
} from './execution_context';
export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types';
export { URL_MAX_LENGTH } from './core_app';
@ -271,6 +278,8 @@ export interface CoreStart {
fatalErrors: FatalErrorsStart;
/** {@link DeprecationsServiceStart} */
deprecations: DeprecationsServiceStart;
/** {@link ExecutionContextServiceStart} */
executionContext: ExecutionContextServiceStart;
/**
* exposed temporarily until https://github.com/elastic/kibana/issues/41990 done
* use *only* to retrieve config values. There is no way to set injected values

View file

@ -25,6 +25,7 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock';
import { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
import { executionContextServiceMock } from './execution_context/execution_context_service.mock';
export { chromeServiceMock } from './chrome/chrome_service.mock';
export { docLinksServiceMock } from './doc_links/doc_links_service.mock';
@ -39,6 +40,7 @@ export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.m
export { scopedHistoryMock } from './application/scoped_history.mock';
export { applicationServiceMock } from './application/application_service.mock';
export { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
export { executionContextServiceMock } from './execution_context/execution_context_service.mock';
function createCoreSetupMock({
basePath = '',
@ -84,6 +86,7 @@ function createCoreStartMock({ basePath = '' } = {}) {
getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar,
},
fatalErrors: fatalErrorsServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createStartContract(),
};
return mock;

View file

@ -140,5 +140,6 @@ export function createPluginStartContext<
},
fatalErrors: deps.fatalErrors,
deprecations: deps.deprecations,
executionContext: deps.executionContext,
};
}

View file

@ -35,6 +35,7 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock';
import { executionContextServiceMock } from '../execution_context/execution_context_service.mock';
export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
@ -103,6 +104,7 @@ describe('PluginsService', () => {
savedObjects: savedObjectsServiceMock.createStartContract(),
fatalErrors: fatalErrorsServiceMock.createStartContract(),
deprecations: deprecationsServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createStartContract(),
};
mockStartContext = {
...mockStartDeps,

View file

@ -433,6 +433,8 @@ export interface CoreStart {
// (undocumented)
docLinks: DocLinksStart;
// (undocumented)
executionContext: ExecutionContextServiceStart;
// (undocumented)
fatalErrors: FatalErrorsStart;
// (undocumented)
http: HttpStart;
@ -714,6 +716,11 @@ export interface ErrorToastOptions extends ToastOptions {
toastMessage?: string;
}
// @public (undocumented)
export interface ExecutionContextServiceStart {
create: (context: KibanaExecutionContext) => IExecutionContextContainer;
}
// @public
export interface FatalErrorInfo {
// (undocumented)
@ -752,6 +759,8 @@ export class HttpFetchError extends Error implements IHttpFetchError {
export interface HttpFetchOptions extends HttpRequestInit {
asResponse?: boolean;
asSystemRequest?: boolean;
// (undocumented)
context?: IExecutionContextContainer;
headers?: HttpHeadersInit;
prependBasePath?: boolean;
query?: HttpFetchQuery;
@ -887,6 +896,12 @@ export interface IBasePath {
readonly serverBasePath: string;
}
// @public (undocumented)
export interface IExecutionContextContainer {
// (undocumented)
toHeader: () => Record<string, string>;
}
// @public
export interface IExternalUrl {
validateUrl(relativeOrAbsoluteUrl: string): URL | null;
@ -949,6 +964,15 @@ export interface IUiSettingsClient {
set: (key: string, value: any) => Promise<boolean>;
}
// @public (undocumented)
export interface KibanaExecutionContext {
readonly description: string;
readonly id: string;
readonly name: string;
readonly type: string;
readonly url?: string;
}
// @public
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
@ -1663,6 +1687,6 @@ export interface UserProvidedValues<T = any> {
// Warnings were encountered during analysis:
//
// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
// src/core/public/core_system.ts:172:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```

View file

@ -10,6 +10,7 @@ import supertest from 'supertest';
import { REPO_ROOT } from '@kbn/dev-utils';
import { HttpService, InternalHttpServiceSetup } from '../../http';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { Env } from '../../config';
import { getEnvOptions } from '../../config/mocks';
@ -31,6 +32,7 @@ describe('CapabilitiesService', () => {
server = createHttpServer();
httpSetup = await server.setup({
context: contextServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
service = new CapabilitiesService({
coreId,

View file

@ -10,6 +10,7 @@ import { resolve } from 'path';
import { readFile } from 'fs/promises';
import supertest from 'supertest';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { HttpService, IRouter } from '../../http';
import { createHttpServer } from '../../http/test_utils';
@ -53,6 +54,7 @@ describe('bundle routes', () => {
it('serves images inside from the bundle path', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''));
@ -70,6 +72,7 @@ describe('bundle routes', () => {
it('serves uncompressed js files', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''));
@ -87,6 +90,7 @@ describe('bundle routes', () => {
it('returns 404 for files outside of the bundlePath', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''));
@ -100,6 +104,7 @@ describe('bundle routes', () => {
it('returns 404 for non-existing files', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''));
@ -113,6 +118,7 @@ describe('bundle routes', () => {
it('returns gzip version if present', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''));
@ -137,6 +143,7 @@ describe('bundle routes', () => {
it('uses max-age cache-control', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''), { isDist: true });
@ -155,6 +162,7 @@ describe('bundle routes', () => {
it('uses etag cache-control', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
registerFooPluginRoute(createRouter(''), { isDist: false });

View file

@ -55,14 +55,19 @@ describe('ClusterClient', () => {
it('creates a single internal and scoped client during initialization', () => {
const config = createConfig();
new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
const getExecutionContextMock = jest.fn();
new ClusterClient(config, logger, 'custom-type', getAuthHeaders, getExecutionContextMock);
expect(configureClientMock).toHaveBeenCalledTimes(2);
expect(configureClientMock).toHaveBeenCalledWith(config, { logger, type: 'custom-type' });
expect(configureClientMock).toHaveBeenCalledWith(config, {
logger,
type: 'custom-type',
getExecutionContext: getExecutionContextMock,
});
expect(configureClientMock).toHaveBeenCalledWith(config, {
logger,
type: 'custom-type',
getExecutionContext: getExecutionContextMock,
scoped: true,
});
});

View file

@ -10,6 +10,7 @@ import { Client } from '@elastic/elasticsearch';
import { Logger } from '../../logging';
import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http';
import { ensureRawRequest, filterHeaders } from '../../http/router';
import type { IExecutionContextContainer } from '../../execution_context';
import { ScopeableRequest } from '../types';
import { ElasticsearchClient } from './types';
import { configureClient } from './configure_client';
@ -54,6 +55,7 @@ export interface ICustomClusterClient extends IClusterClient {
export class ClusterClient implements ICustomClusterClient {
public readonly asInternalUser: Client;
private readonly rootScopedClient: Client;
private readonly allowListHeaders: string[];
private isClosed = false;
@ -61,10 +63,18 @@ export class ClusterClient implements ICustomClusterClient {
private readonly config: ElasticsearchClientConfig,
logger: Logger,
type: string,
private readonly getAuthHeaders: GetAuthHeaders = noop
private readonly getAuthHeaders: GetAuthHeaders = noop,
getExecutionContext: () => IExecutionContextContainer | undefined = noop
) {
this.asInternalUser = configureClient(config, { logger, type });
this.rootScopedClient = configureClient(config, { logger, type, scoped: true });
this.asInternalUser = configureClient(config, { logger, type, getExecutionContext });
this.rootScopedClient = configureClient(config, {
logger,
type,
getExecutionContext,
scoped: true,
});
this.allowListHeaders = ['x-opaque-id', ...this.config.requestHeadersWhitelist];
}
asScoped(request: ScopeableRequest) {
@ -90,10 +100,10 @@ export class ClusterClient implements ICustomClusterClient {
const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {};
const authHeaders = this.getAuthHeaders(request);
scopedHeaders = filterHeaders({ ...requestHeaders, ...requestIdHeaders, ...authHeaders }, [
'x-opaque-id',
...this.config.requestHeadersWhitelist,
]);
scopedHeaders = filterHeaders(
{ ...requestHeaders, ...requestIdHeaders, ...authHeaders },
this.allowListHeaders
);
} else {
scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist);
}

View file

@ -98,7 +98,7 @@ describe('configureClient', () => {
const client = configureClient(config, { logger, type: 'test', scoped: false });
expect(ClientMock).toHaveBeenCalledTimes(1);
expect(ClientMock).toHaveBeenCalledWith(parsedOptions);
expect(ClientMock).toHaveBeenCalledWith(expect.objectContaining(parsedOptions));
expect(client).toBe(ClientMock.mock.results[0].value);
});

View file

@ -8,18 +8,46 @@
import { Buffer } from 'buffer';
import { stringify } from 'querystring';
import { ApiError, Client, RequestEvent, errors } from '@elastic/elasticsearch';
import type { RequestBody } from '@elastic/elasticsearch/lib/Transport';
import { ApiError, Client, RequestEvent, errors, Transport } from '@elastic/elasticsearch';
import type {
RequestBody,
TransportRequestParams,
TransportRequestOptions,
} from '@elastic/elasticsearch/lib/Transport';
import type { IExecutionContextContainer } from '../../execution_context';
import { Logger } from '../../logging';
import { parseClientOptions, ElasticsearchClientConfig } from './client_config';
const noop = () => undefined;
export const configureClient = (
config: ElasticsearchClientConfig,
{ logger, type, scoped = false }: { logger: Logger; type: string; scoped?: boolean }
{
logger,
type,
scoped = false,
getExecutionContext = noop,
}: {
logger: Logger;
type: string;
scoped?: boolean;
getExecutionContext?: () => IExecutionContextContainer | undefined;
}
): Client => {
const clientOptions = parseClientOptions(config, scoped);
class KibanaTransport extends Transport {
request(params: TransportRequestParams, options?: TransportRequestOptions) {
const opts = options || {};
const opaqueId = getExecutionContext()?.toString();
if (opaqueId && !opts.opaqueId) {
// rewrites headers['x-opaque-id'] if it presents
opts.opaqueId = opaqueId;
}
return super.request(params, opts);
}
}
const client = new Client(clientOptions);
const client = new Client({ ...clientOptions, Transport: KibanaTransport });
addLogging(client, logger.get('query', type));
return client;

View file

@ -15,6 +15,7 @@ import { configServiceMock, getEnvOptions } from '../config/mocks';
import { CoreContext } from '../core_context';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { executionContextServiceMock } from '../execution_context/execution_context_service.mock';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
import { elasticsearchServiceMock } from './elasticsearch_service.mock';
@ -28,6 +29,7 @@ let elasticsearchService: ElasticsearchService;
const configService = configServiceMock.create();
const setupDeps = {
http: httpServiceMock.createInternalSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
configService.atPath.mockReturnValue(
new BehaviorSubject({
@ -274,12 +276,7 @@ describe('#start', () => {
expect(clusterClient).toBe(mockClusterClientInstance);
expect(MockClusterClient).toHaveBeenCalledTimes(1);
expect(MockClusterClient).toHaveBeenCalledWith(
expect.objectContaining(customConfig),
expect.objectContaining({ context: ['elasticsearch'] }),
'custom-type',
expect.any(Function)
);
expect(MockClusterClient.mock.calls[0][0]).toEqual(expect.objectContaining(customConfig));
});
it('creates a new client on each call', async () => {
await elasticsearchService.setup(setupDeps);

View file

@ -20,13 +20,15 @@ import {
} from './legacy';
import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client';
import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config';
import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/';
import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http/';
import type { InternalExecutionContextSetup, IExecutionContext } from '../execution_context';
import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types';
import { pollEsNodesVersion } from './version_check/ensure_es_version';
import { calculateStatus$ } from './status';
interface SetupDeps {
http: InternalHttpServiceSetup;
executionContext: InternalExecutionContextSetup;
}
/** @internal */
@ -37,6 +39,7 @@ export class ElasticsearchService
private stop$ = new Subject();
private kibanaVersion: string;
private getAuthHeaders?: GetAuthHeaders;
private executionContextClient?: IExecutionContext;
private createLegacyCustomClient?: (
type: string,
@ -60,6 +63,7 @@ export class ElasticsearchService
const config = await this.config$.pipe(first()).toPromise();
this.getAuthHeaders = deps.http.getAuthHeaders;
this.executionContextClient = deps.executionContext;
this.legacyClient = this.createLegacyClusterClient('data', config);
this.client = this.createClusterClient('data', config);
@ -128,7 +132,8 @@ export class ElasticsearchService
config,
this.coreContext.logger.get('elasticsearch'),
type,
this.getAuthHeaders
this.getAuthHeaders,
() => this.executionContextClient?.get()
);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TypeOf, schema } from '@kbn/config-schema';
import { ServiceConfigDescriptor } from '../internal_types';
const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});
/**
* @internal
*/
export type ExecutionContextConfigType = TypeOf<typeof configSchema>;
export const config: ServiceConfigDescriptor<ExecutionContextConfigType> = {
path: 'execution_context',
schema: configSchema,
};

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaServerExecutionContext } from './execution_context_service';
import {
ExecutionContextContainer,
getParentContextFrom,
BAGGAGE_HEADER,
BAGGAGE_MAX_PER_NAME_VALUE_PAIRS,
} from './execution_context_container';
describe('KibanaExecutionContext', () => {
describe('toString', () => {
it('returns a string representation of provided execution context', () => {
const context: KibanaServerExecutionContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'test-descripton',
requestId: '1234-5678',
};
const value = new ExecutionContextContainer(context).toString();
expect(value).toMatchInlineSnapshot(`"1234-5678;kibana:test-type:42"`);
});
it('returns a limited representation if optional properties are omitted', () => {
const context: KibanaServerExecutionContext = {
requestId: '1234-5678',
};
const value = new ExecutionContextContainer(context).toString();
expect(value).toMatchInlineSnapshot(`"1234-5678"`);
});
it('trims a string representation of provided execution context if it is bigger max allowed size', () => {
expect(
new Blob([
new ExecutionContextContainer({
requestId: '1234-5678'.repeat(1000),
}).toString(),
]).size
).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS);
expect(
new Blob([
new ExecutionContextContainer({
type: 'test-type'.repeat(1000),
name: 'test-name',
id: '42'.repeat(1000),
description: 'test-descripton',
requestId: '1234-5678',
}).toString(),
]).size
).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS);
});
});
describe('toJSON', () => {
it('returns a context object', () => {
const context: KibanaServerExecutionContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'test-descripton',
requestId: '1234-5678',
};
const value = new ExecutionContextContainer(context).toJSON();
expect(value).toBe(context);
});
});
});
describe('getParentContextFrom', () => {
it('decodes provided header', () => {
const ctx = { id: '42' };
const header = encodeURIComponent(JSON.stringify(ctx));
expect(getParentContextFrom({ [BAGGAGE_HEADER]: header })).toEqual(ctx);
});
it('does not throw an exception if given not a valid value', () => {
expect(getParentContextFrom({ [BAGGAGE_HEADER]: 'value' })).toBeUndefined();
expect(getParentContextFrom({ [BAGGAGE_HEADER]: '' })).toBeUndefined();
expect(getParentContextFrom({})).toBeUndefined();
const ctx = { id: '42' };
const header = encodeURIComponent(JSON.stringify(ctx));
expect(getParentContextFrom({ [BAGGAGE_HEADER]: header.slice(0, -2) })).toBeUndefined();
});
});

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaServerExecutionContext } from './execution_context_service';
import type { KibanaExecutionContext } from '../../types';
// Switch to the standard Baggage header. blocked by
// https://github.com/elastic/apm-agent-nodejs/issues/2102
export const BAGGAGE_HEADER = 'x-kbn-context';
export function getParentContextFrom(
headers: Record<string, string>
): KibanaExecutionContext | undefined {
const header = headers[BAGGAGE_HEADER];
return parseHeader(header);
}
function parseHeader(header?: string): KibanaExecutionContext | undefined {
if (!header) return undefined;
try {
return JSON.parse(decodeURIComponent(header));
} catch (e) {
return undefined;
}
}
// Maximum number of bytes per a single name-value pair allowed by w3c spec
// https://w3c.github.io/baggage/
export const BAGGAGE_MAX_PER_NAME_VALUE_PAIRS = 4096;
// a single character can use up to 4 bytes
const MAX_BAGGAGE_LENGTH = BAGGAGE_MAX_PER_NAME_VALUE_PAIRS / 4;
// Limits the header value to max allowed "baggage" header property name-value pair
// It will help us switch to the "baggage" header when it becomes the standard.
// The trimmed value in the logs is better than nothing.
function enforceMaxLength(header: string): string {
return header.slice(0, MAX_BAGGAGE_LENGTH);
}
/**
* @public
*/
export interface IExecutionContextContainer {
toString(): string;
toJSON(): Readonly<KibanaServerExecutionContext>;
}
export class ExecutionContextContainer implements IExecutionContextContainer {
readonly #context: Readonly<KibanaServerExecutionContext>;
constructor(context: Readonly<KibanaServerExecutionContext>) {
this.#context = context;
}
toString(): string {
const ctx = this.#context;
const contextStringified = ctx.type && ctx.id ? `kibana:${ctx.type}:${ctx.id}` : '';
const result = contextStringified ? `${ctx.requestId};${contextStringified}` : ctx.requestId;
return enforceMaxLength(result);
}
toJSON(): Readonly<KibanaServerExecutionContext> {
return this.#context;
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
IExecutionContext,
InternalExecutionContextSetup,
ExecutionContextSetup,
} from './execution_context_service';
const createExecutionContextMock = () => {
const mock: jest.Mocked<IExecutionContext> = {
set: jest.fn(),
reset: jest.fn(),
get: jest.fn(),
getParentContextFrom: jest.fn(),
};
return mock;
};
const createInternalSetupContractMock = () => {
const setupContract: jest.Mocked<InternalExecutionContextSetup> = createExecutionContextMock();
return setupContract;
};
const createSetupContractMock = () => {
const mock: jest.Mocked<ExecutionContextSetup> = {
set: jest.fn(),
get: jest.fn(),
};
return mock;
};
export const executionContextServiceMock = {
createInternalSetupContract: createInternalSetupContractMock,
createInternalStartContract: createInternalSetupContractMock,
createSetupContract: createSetupContractMock,
createStartContract: createSetupContractMock,
};

View file

@ -0,0 +1,131 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
import {
ExecutionContextService,
InternalExecutionContextSetup,
} from './execution_context_service';
import { mockCoreContext } from '../core_context.mock';
const delay = (ms: number = 100) => new Promise((resolve) => setTimeout(resolve, ms));
describe('ExecutionContextService', () => {
describe('setup', () => {
let service: InternalExecutionContextSetup;
const core = mockCoreContext.create();
core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: true }));
beforeEach(() => {
service = new ExecutionContextService(core).setup();
});
it('sets and gets a value in async context', async () => {
const chainA = Promise.resolve().then(async () => {
service.set({
requestId: '0000',
});
await delay(500);
return service.get();
});
const chainB = Promise.resolve().then(async () => {
service.set({
requestId: '1111',
});
await delay(100);
return service.get();
});
expect(
await Promise.all([chainA, chainB]).then((results) =>
results.map((result) => result?.toJSON())
)
).toEqual([
{
requestId: '0000',
},
{
requestId: '1111',
},
]);
});
it('sets and resets a value in async context', async () => {
const chainA = Promise.resolve().then(async () => {
service.set({
requestId: '0000',
});
await delay(500);
service.reset();
return service.get();
});
const chainB = Promise.resolve().then(async () => {
service.set({
requestId: '1111',
});
await delay(100);
return service.get();
});
expect(
await Promise.all([chainA, chainB]).then((results) =>
results.map((result) => result?.toJSON())
)
).toEqual([
undefined,
{
requestId: '1111',
},
]);
});
});
describe('config', () => {
it('can be disabled', async () => {
const core = mockCoreContext.create();
core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: false }));
const service = new ExecutionContextService(core).setup();
const chainA = await Promise.resolve().then(async () => {
service.set({
requestId: '0000',
});
await delay(100);
return service.get();
});
expect(chainA).toBeUndefined();
});
it('reacts to config changes', async () => {
const core = mockCoreContext.create();
const config$ = new BehaviorSubject({ enabled: false });
core.configService.atPath.mockReturnValue(config$);
const service = new ExecutionContextService(core).setup();
function exec() {
return Promise.resolve().then(async () => {
service.set({
requestId: '0000',
});
await delay(100);
return service.get();
});
}
expect(await exec()).toBeUndefined();
config$.next({
enabled: true,
});
expect(await exec()).toBeDefined();
config$.next({
enabled: false,
});
expect(await exec()).toBeUndefined();
});
});
});

View file

@ -0,0 +1,135 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AsyncLocalStorage } from 'async_hooks';
import type { Subscription } from 'rxjs';
import type { CoreService, KibanaExecutionContext } from '../../types';
import type { CoreContext } from '../core_context';
import type { Logger } from '../logging';
import type { ExecutionContextConfigType } from './execution_context_config';
import {
ExecutionContextContainer,
IExecutionContextContainer,
getParentContextFrom,
} from './execution_context_container';
/**
* @public
*/
export interface KibanaServerExecutionContext extends Partial<KibanaExecutionContext> {
requestId: string;
}
/**
* @internal
*/
export interface IExecutionContext {
getParentContextFrom(headers: Record<string, string>): KibanaExecutionContext | undefined;
set(context: Partial<KibanaServerExecutionContext>): void;
reset(): void;
get(): IExecutionContextContainer | undefined;
}
/**
* @internal
*/
export type InternalExecutionContextSetup = IExecutionContext;
/**
* @internal
*/
export type InternalExecutionContextStart = IExecutionContext;
/**
* @public
*/
export interface ExecutionContextSetup {
/**
* Stores the meta-data of a runtime operation.
* Data are carried over all async operations automatically.
* The sequential calls merge provided "context" object shallowly.
**/
set(context: Partial<KibanaServerExecutionContext>): void;
/**
* Retrieves an opearation meta-data for the current async context.
**/
get(): IExecutionContextContainer | undefined;
}
/**
* @public
*/
export type ExecutionContextStart = ExecutionContextSetup;
export class ExecutionContextService
implements CoreService<InternalExecutionContextSetup, InternalExecutionContextStart> {
private readonly log: Logger;
private readonly asyncLocalStorage: AsyncLocalStorage<IExecutionContextContainer>;
private enabled = false;
private configSubscription?: Subscription;
constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('execution_context');
this.asyncLocalStorage = new AsyncLocalStorage<IExecutionContextContainer>();
}
setup(): InternalExecutionContextSetup {
this.configSubscription = this.coreContext.configService
.atPath<ExecutionContextConfigType>('execution_context')
.subscribe((config) => {
this.enabled = config.enabled;
});
return {
getParentContextFrom,
set: this.set.bind(this),
reset: this.reset.bind(this),
get: this.get.bind(this),
};
}
start(): InternalExecutionContextStart {
return {
getParentContextFrom,
set: this.set.bind(this),
reset: this.reset.bind(this),
get: this.get.bind(this),
};
}
stop() {
this.enabled = false;
if (this.configSubscription) {
this.configSubscription.unsubscribe();
this.configSubscription = undefined;
}
}
private set(context: KibanaServerExecutionContext) {
if (!this.enabled) return;
const prevValue = this.asyncLocalStorage.getStore();
// merges context objects shallowly. repeats the deafult logic of apm.setCustomContext(ctx)
const contextContainer = new ExecutionContextContainer({ ...prevValue?.toJSON(), ...context });
// we have to use enterWith since Hapi lifecycle model is built on event emitters.
// therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles.
this.asyncLocalStorage.enterWith(contextContainer);
this.log.trace(`stored the execution context: ${contextContainer.toJSON()}`);
}
private reset() {
if (!this.enabled) return;
// @ts-expect-error "undefined" is not supported in type definitions, which is wrong
this.asyncLocalStorage.enterWith(undefined);
}
private get(): IExecutionContextContainer | undefined {
if (!this.enabled) return;
return this.asyncLocalStorage.getStore();
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { KibanaExecutionContext } from '../../types';
export { ExecutionContextService } from './execution_context_service';
export type {
InternalExecutionContextSetup,
InternalExecutionContextStart,
ExecutionContextSetup,
ExecutionContextStart,
IExecutionContext,
KibanaServerExecutionContext,
} from './execution_context_service';
export type { IExecutionContextContainer } from './execution_context_container';
export { config } from './execution_context_config';

View file

@ -0,0 +1,542 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ExecutionContextContainer } from '../../../public/execution_context/execution_context_container';
import * as kbnTestServer from '../../../test_helpers/kbn_server';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const parentContext = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'test-description',
};
describe('trace', () => {
let esServer: kbnTestServer.TestElasticsearchUtils;
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeAll(async () => {
const { startES } = kbnTestServer.createTestServers({
adjustTimeout: jest.setTimeout,
});
esServer = await startES();
});
afterAll(async () => {
await esServer.stop();
});
beforeEach(async () => {
root = kbnTestServer.createRootWithCorePlugins({
plugins: { initialize: false },
server: {
requestId: {
allowFromAnyIp: true,
},
},
});
}, 30000);
afterEach(async () => {
await root.shutdown();
});
describe('x-opaque-id', () => {
it('passed to Elasticsearch unscoped client calls', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asInternalUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe(myOpaqueId);
});
it('passed to Elasticsearch scoped client calls', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe(myOpaqueId);
});
it('generated and attached to Elasticsearch unscoped client calls if not specifed', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asInternalUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const response = await kbnTestServer.request.get(root, '/execution-context').expect(200);
const header = response.body['x-opaque-id'];
expect(header).toEqual(expect.any(String));
});
it('generated and attached to Elasticsearch scoped client calls if not specifed', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const response = await kbnTestServer.request.get(root, '/execution-context').expect(200);
const header = response.body['x-opaque-id'];
expect(header).toEqual(expect.any(String));
});
it('can be overriden during Elasticsearch client call', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asInternalUser.ping(
{},
{
opaqueId: 'new-opaque-id',
}
);
return res.ok({ body: headers || {} });
});
await root.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe('new-opaque-id');
});
describe('ExecutionContext Service is disabled', () => {
let rootExecutionContextDisabled: ReturnType<typeof kbnTestServer.createRoot>;
beforeEach(async () => {
rootExecutionContextDisabled = kbnTestServer.createRootWithCorePlugins({
execution_context: { enabled: false },
plugins: { initialize: false },
server: {
requestId: {
allowFromAnyIp: true,
},
},
});
}, 30000);
afterEach(async () => {
await rootExecutionContextDisabled.shutdown();
});
it('passed to Elasticsearch scoped client calls even if ExecutionContext Service is disabled', async () => {
const { http } = await rootExecutionContextDisabled.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await rootExecutionContextDisabled.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(rootExecutionContextDisabled, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe(myOpaqueId);
});
it('does not pass context if ExecutionContext Service is disabled', async () => {
const { http, executionContext } = await rootExecutionContextDisabled.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({
body: { context: executionContext.get()?.toJSON(), header: headers?.['x-opaque-id'] },
});
});
await rootExecutionContextDisabled.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(rootExecutionContextDisabled, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
expect(response.body).toEqual({
header: 'my-opaque-id',
});
});
});
});
describe('execution context', () => {
it('sets execution context for a sync request handler', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
return res.ok({ body: executionContext.get() });
});
await root.start();
const response = await kbnTestServer.request.get(root, '/execution-context').expect(200);
expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) });
});
it('sets execution context for an async request handler', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
await delay(100);
return res.ok({ body: executionContext.get() });
});
await root.start();
const response = await kbnTestServer.request.get(root, '/execution-context').expect(200);
expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) });
});
it('execution context is uniq for sequential requests', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
await delay(100);
return res.ok({ body: executionContext.get() });
});
await root.start();
const responseA = await kbnTestServer.request.get(root, '/execution-context').expect(200);
const responseB = await kbnTestServer.request.get(root, '/execution-context').expect(200);
expect(responseA.body).toEqual({ ...parentContext, requestId: expect.any(String) });
expect(responseB.body).toEqual({ ...parentContext, requestId: expect.any(String) });
expect(responseA.body.requestId).not.toBe(responseB.body.requestId);
});
it('execution context is uniq for concurrent requests', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
let id = 2;
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
await delay(id-- * 100);
return res.ok({ body: executionContext.get() });
});
await root.start();
const responseA = kbnTestServer.request.get(root, '/execution-context');
const responseB = kbnTestServer.request.get(root, '/execution-context');
const responseC = kbnTestServer.request.get(root, '/execution-context');
const [{ body: bodyA }, { body: bodyB }, { body: bodyC }] = await Promise.all([
responseA,
responseB,
responseC,
]);
expect(bodyA.requestId).toBeDefined();
expect(bodyB.requestId).toBeDefined();
expect(bodyC.requestId).toBeDefined();
expect(bodyA.requestId).not.toBe(bodyB.requestId);
expect(bodyB.requestId).not.toBe(bodyC.requestId);
expect(bodyA.requestId).not.toBe(bodyC.requestId);
});
it('execution context is uniq for concurrent requests when "x-opaque-id" provided', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
let id = 2;
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
await delay(id-- * 100);
return res.ok({ body: executionContext.get() });
});
await root.start();
const responseA = kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', 'req-1');
const responseB = kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', 'req-2');
const responseC = kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', 'req-3');
const [{ body: bodyA }, { body: bodyB }, { body: bodyC }] = await Promise.all([
responseA,
responseB,
responseC,
]);
expect(bodyA.requestId).toBe('req-1');
expect(bodyB.requestId).toBe('req-2');
expect(bodyC.requestId).toBe('req-3');
});
it('parses the parent context if present', async () => {
const { executionContext, http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, (context, req, res) =>
res.ok({ body: executionContext.get() })
);
await root.start();
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set(new ExecutionContextContainer(parentContext).toHeader())
.expect(200);
expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) });
});
it('execution context is the same for all the lifecycle events', async () => {
const { executionContext, http } = await root.setup();
const {
createRouter,
registerOnPreRouting,
registerOnPreAuth,
registerAuth,
registerOnPostAuth,
registerOnPreResponse,
} = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
return res.ok({ body: executionContext.get()?.toJSON() });
});
let onPreRoutingContext;
registerOnPreRouting((request, response, t) => {
onPreRoutingContext = executionContext.get()?.toJSON();
return t.next();
});
let onPreAuthContext;
registerOnPreAuth((request, response, t) => {
onPreAuthContext = executionContext.get()?.toJSON();
return t.next();
});
let authContext;
registerAuth((request, response, t) => {
authContext = executionContext.get()?.toJSON();
return t.authenticated();
});
let onPostAuthContext;
registerOnPostAuth((request, response, t) => {
onPostAuthContext = executionContext.get()?.toJSON();
return t.next();
});
let onPreResponseContext;
registerOnPreResponse((request, response, t) => {
onPreResponseContext = executionContext.get()?.toJSON();
return t.next();
});
await root.start();
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set(new ExecutionContextContainer(parentContext).toHeader())
.expect(200);
expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) });
expect(response.body).toEqual(onPreRoutingContext);
expect(response.body).toEqual(onPreAuthContext);
expect(response.body).toEqual(authContext);
expect(response.body).toEqual(onPostAuthContext);
expect(response.body).toEqual(onPreResponseContext);
});
it('propagates context to Elasticsearch scoped client', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set(new ExecutionContextContainer(parentContext).toHeader())
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toContain('kibana:test-type:42');
});
it('propagates context to Elasticsearch unscoped client', async () => {
const { http } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
const { headers } = await context.core.elasticsearch.client.asInternalUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set(new ExecutionContextContainer(parentContext).toHeader())
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toContain('kibana:test-type:42');
});
it('a repeat call overwrites the old context', async () => {
const { http, executionContext } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
const newContext = {
type: 'new-type',
name: 'new-name',
id: '41',
description: 'new-description',
};
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(newContext);
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set(new ExecutionContextContainer(parentContext).toHeader())
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toContain('kibana:new-type:41');
});
it('does not affect "x-opaque-id" set by user', async () => {
const { http, executionContext } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(parentContext);
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe('my-opaque-id;kibana:test-type:42');
});
it('does not break on non-ASCII characters within execution context', async () => {
const { http, executionContext } = await root.setup();
const { createRouter } = http;
const router = createRouter('');
const ctx = {
type: 'test-type',
name: 'test-name',
id: '42',
description: 'какое-то описание',
};
router.get({ path: '/execution-context', validate: false }, async (context, req, res) => {
executionContext.set(ctx);
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return res.ok({ body: headers || {} });
});
await root.start();
const myOpaqueId = 'my-opaque-id';
const response = await kbnTestServer.request
.get(root, '/execution-context')
.set('x-opaque-id', myOpaqueId)
.expect(200);
const header = response.body['x-opaque-id'];
expect(header).toBe('my-opaque-id;kibana:test-type:42');
});
});
});

View file

@ -18,6 +18,7 @@ import { KibanaRequest } from './router';
import { Env } from '../config';
import { contextServiceMock } from '../context/context_service.mock';
import { executionContextServiceMock } from '../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { getEnvOptions, configServiceMock } from '../config/mocks';
import { httpServerMock } from './http_server.mocks';
@ -34,6 +35,7 @@ const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
configService.atPath.mockImplementation((path) => {

View file

@ -22,6 +22,7 @@ import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
import type { InternalExecutionContextSetup } from '../execution_context';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
@ -133,18 +134,23 @@ export class HttpServer {
}
}
public async setup(config: HttpConfig): Promise<HttpServerSetup> {
public async setup(
config: HttpConfig,
executionContext?: InternalExecutionContextSetup
): Promise<HttpServerSetup> {
const serverOptions = getServerOptions(config);
const listenerOptions = getListenerOptions(config);
this.server = createServer(serverOptions, listenerOptions);
await this.server.register([HapiStaticFiles]);
this.config = config;
// It's important to have setupRequestStateAssignment call the very first, otherwise context passing will be broken.
// That's the only reason why context initialization exists in this method.
this.setupRequestStateAssignment(config, executionContext);
const basePathService = new BasePath(config.basePath, config.publicBaseUrl);
this.setupBasePathRewrite(config, basePathService);
this.setupConditionalCompression(config);
this.setupResponseLogging();
this.setupRequestStateAssignment(config);
this.setupGracefulShutdownHandlers();
return {
@ -323,11 +329,22 @@ export class HttpServer {
this.server.events.on('response', this.handleServerResponseEvent);
}
private setupRequestStateAssignment(config: HttpConfig) {
private setupRequestStateAssignment(
config: HttpConfig,
executionContext?: InternalExecutionContextSetup
) {
this.server!.ext('onRequest', (request, responseToolkit) => {
const requestId = getRequestId(request, config.requestId);
const parentContext = executionContext?.getParentContextFrom(request.headers);
executionContext?.set({
...parentContext,
requestId,
});
request.app = {
...(request.app ?? {}),
requestId: getRequestId(request, config.requestId),
requestId,
requestUuid: uuid.v4(),
} as KibanaRequestState;
return responseToolkit.continue;

View file

@ -18,6 +18,7 @@ import { httpServerMock } from './http_server.mocks';
import { ConfigService, Env } from '../config';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { contextServiceMock } from '../context/context_service.mock';
import { executionContextServiceMock } from '../execution_context/execution_context_service.mock';
import { config as cspConfig } from '../csp';
import { config as externalUrlConfig } from '../external_url';
@ -45,6 +46,7 @@ const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
const fakeHapiServer = {
start: noop,

View file

@ -11,6 +11,7 @@ import { first, map } from 'rxjs/operators';
import { pick } from '@kbn/std';
import type { RequestHandlerContext } from 'src/core/server';
import type { InternalExecutionContextSetup } from '../execution_context';
import { CoreService } from '../../types';
import { Logger, LoggerFactory } from '../logging';
import { ContextSetup } from '../context';
@ -41,6 +42,7 @@ import {
interface SetupDeps {
context: ContextSetup;
executionContext: InternalExecutionContextSetup;
}
/** @internal */
@ -90,7 +92,10 @@ export class HttpService
const notReadyServer = await this.setupNotReadyService({ config, context: deps.context });
const { registerRouter, ...serverContract } = await this.httpServer.setup(config);
const { registerRouter, ...serverContract } = await this.httpServer.setup(
config,
deps.executionContext
);
registerCoreHandlers(serverContract, config, this.env);

View file

@ -14,6 +14,7 @@ import { ensureRawRequest } from '../router';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
@ -25,6 +26,7 @@ const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
beforeEach(() => {

View file

@ -18,6 +18,7 @@ import { IRouter, RouteRegistrar } from '../router';
import { configServiceMock } from '../../config/mocks';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../../../../package.json');
@ -31,6 +32,7 @@ const xsrfDisabledTestPath = '/xsrf/test/route/disabled';
const kibanaName = 'my-kibana-name';
const setupDeps = {
context: contextServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
describe('core lifecycle handlers', () => {

View file

@ -15,6 +15,7 @@ import supertest from 'supertest';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
import { schema } from '@kbn/config-schema';
@ -26,6 +27,7 @@ const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
beforeEach(() => {

View file

@ -12,6 +12,7 @@ import supertest from 'supertest';
import { schema } from '@kbn/config-schema';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
import { HttpService } from '../http_service';
@ -24,6 +25,7 @@ const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
beforeEach(() => {

View file

@ -78,6 +78,16 @@ export type {
ConfigUsageData,
};
import type { ExecutionContextSetup, ExecutionContextStart } from './execution_context';
export type {
ExecutionContextSetup,
ExecutionContextStart,
IExecutionContextContainer,
KibanaServerExecutionContext,
KibanaExecutionContext,
} from './execution_context';
export { bootstrap } from './bootstrap';
export type {
Capabilities,
@ -475,6 +485,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
context: ContextSetup;
/** {@link ElasticsearchServiceSetup} */
elasticsearch: ElasticsearchServiceSetup;
/** {@link ExecutionContextSetup} */
executionContext: ExecutionContextSetup;
/** {@link HttpServiceSetup} */
http: HttpServiceSetup & {
/** {@link HttpResources} */
@ -521,6 +533,8 @@ export interface CoreStart {
capabilities: CapabilitiesStart;
/** {@link ElasticsearchServiceStart} */
elasticsearch: ElasticsearchServiceStart;
/** {@link ExecutionContextStart} */
executionContext: ExecutionContextStart;
/** {@link HttpServiceStart} */
http: HttpServiceStart;
/** {@link MetricsServiceStart} */

View file

@ -30,6 +30,10 @@ import { InternalLoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
import { InternalDeprecationsServiceSetup } from './deprecations';
import type {
InternalExecutionContextSetup,
InternalExecutionContextStart,
} from './execution_context';
/** @internal */
export interface InternalCoreSetup {
@ -37,6 +41,7 @@ export interface InternalCoreSetup {
context: ContextSetup;
http: InternalHttpServiceSetup;
elasticsearch: InternalElasticsearchServiceSetup;
executionContext: InternalExecutionContextSetup;
i18n: I18nServiceSetup;
savedObjects: InternalSavedObjectsServiceSetup;
status: InternalStatusServiceSetup;
@ -60,6 +65,7 @@ export interface InternalCoreStart {
savedObjects: InternalSavedObjectsServiceStart;
uiSettings: InternalUiSettingsServiceStart;
coreUsageData: CoreUsageDataStart;
executionContext: InternalExecutionContextStart;
}
/**

View file

@ -13,6 +13,7 @@ import { Server as HapiServer } from '@hapi/hapi';
import { createHttpServer } from '../../http/test_utils';
import { HttpService, IRouter } from '../../http';
import { contextServiceMock } from '../../context/context_service.mock';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { ServerMetricsCollector } from '../collectors/server';
const requestWaitDelay = 25;
@ -29,7 +30,10 @@ describe('ServerMetricsCollector', () => {
beforeEach(async () => {
server = createHttpServer();
const contextSetup = contextServiceMock.createSetupContract();
const httpSetup = await server.setup({ context: contextSetup });
const httpSetup = await server.setup({
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
hapiServer = httpSetup.server;
router = httpSetup.createRouter('/');
collector = new ServerMetricsCollector(hapiServer);

View file

@ -30,6 +30,7 @@ import { statusServiceMock } from './status/status_service.mock';
import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
import { i18nServiceMock } from './i18n/i18n_service.mock';
import { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
import { executionContextServiceMock } from './execution_context/execution_context_service.mock';
export { configServiceMock } from './config/mocks';
export { httpServerMock } from './http/http_server.mocks';
@ -51,6 +52,7 @@ export { capabilitiesServiceMock } from './capabilities/capabilities_service.moc
export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
export { i18nServiceMock } from './i18n/i18n_service.mock';
export { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
export { executionContextServiceMock } from './execution_context/execution_context_service.mock';
type MockedPluginInitializerConfig<T> = jest.Mocked<PluginInitializerContext<T>['config']>;
@ -144,6 +146,7 @@ function createCoreSetupMock({
logging: loggingServiceMock.createSetupContract(),
metrics: metricsServiceMock.createSetupContract(),
deprecations: deprecationsServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
getStartServices: jest
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>()
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
@ -161,6 +164,7 @@ function createCoreStartMock() {
savedObjects: savedObjectsServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createInternalStartContract(),
};
return mock;
@ -182,6 +186,7 @@ function createInternalCoreSetupMock() {
logging: loggingServiceMock.createInternalSetupContract(),
metrics: metricsServiceMock.createInternalSetupContract(),
deprecations: deprecationsServiceMock.createInternalSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
return setupDeps;
}
@ -195,6 +200,7 @@ function createInternalCoreStartMock() {
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createInternalStartContract(),
};
return startDeps;
}

View file

@ -115,6 +115,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
elasticsearch: {
legacy: deps.elasticsearch.legacy,
},
executionContext: deps.executionContext,
http: {
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
registerRouteHandlerContext: <
@ -195,6 +196,7 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
createClient: deps.elasticsearch.createClient,
legacy: deps.elasticsearch.legacy,
},
executionContext: deps.executionContext,
http: {
auth: deps.http.auth,
basePath: deps.http.basePath,

View file

@ -11,6 +11,7 @@ import { registerGetRoute } from '../get';
import { ContextService } from '../../../context';
import { savedObjectsClientMock } from '../../service/saved_objects_client.mock';
import { CoreUsageStatsClient } from '../../../core_usage_data';
import { executionContextServiceMock } from '../../../execution_context/execution_context_service.mock';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { HttpService, InternalHttpServiceSetup } from '../../../http';
@ -33,6 +34,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
const contextService = new ContextService(coreContext);
httpSetup = await server.setup({
context: contextService.setup({ pluginDependencies: new Map() }),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
handlerContext = coreMock.createRequestHandlerContext();

View file

@ -13,6 +13,7 @@ import { savedObjectsClientMock } from '../../service/saved_objects_client.mock'
import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { executionContextServiceMock } from '../../../execution_context/execution_context_service.mock';
import { HttpService, InternalHttpServiceSetup } from '../../../http';
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
import { coreMock } from '../../../mocks';
@ -33,6 +34,7 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
const contextService = new ContextService(coreContext);
httpSetup = await server.setup({
context: contextService.setup({ pluginDependencies: new Map() }),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
handlerContext = coreMock.createRequestHandlerContext();

View file

@ -9,6 +9,7 @@
import { ContextService } from '../../context';
import { createHttpServer, createCoreContext } from '../../http/test_utils';
import { coreMock } from '../../mocks';
import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock';
import { SavedObjectsType } from '../types';
const defaultCoreId = Symbol('core');
@ -20,6 +21,7 @@ export const setupServer = async (coreId: symbol = defaultCoreId) => {
const server = createHttpServer(coreContext);
const httpSetup = await server.setup({
context: contextService.setup({ pluginDependencies: new Map() }),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
const handlerContext = coreMock.createRequestHandlerContext();

View file

@ -523,6 +523,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
// (undocumented)
elasticsearch: ElasticsearchServiceSetup;
// (undocumented)
executionContext: ExecutionContextSetup;
// (undocumented)
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
// (undocumented)
http: HttpServiceSetup & {
@ -551,6 +553,8 @@ export interface CoreStart {
// (undocumented)
elasticsearch: ElasticsearchServiceStart;
// (undocumented)
executionContext: ExecutionContextStart;
// (undocumented)
http: HttpServiceStart;
// (undocumented)
metrics: MetricsServiceStart;
@ -1015,6 +1019,15 @@ export interface ErrorHttpResponseOptions {
headers?: ResponseHeaders;
}
// @public (undocumented)
export interface ExecutionContextSetup {
get(): IExecutionContextContainer | undefined;
set(context: Partial<KibanaServerExecutionContext>): void;
}
// @public (undocumented)
export type ExecutionContextStart = ExecutionContextSetup;
// @public
export interface FakeRequest {
headers: Headers;
@ -1187,6 +1200,14 @@ export interface ICustomClusterClient extends IClusterClient {
close: () => Promise<void>;
}
// @public (undocumented)
export interface IExecutionContextContainer {
// (undocumented)
toJSON(): Readonly<KibanaServerExecutionContext>;
// (undocumented)
toString(): string;
}
// @public
export interface IExternalUrlConfig {
readonly policy: IExternalUrlPolicy[];
@ -1303,6 +1324,15 @@ export interface IUiSettingsClient {
setMany: (changes: Record<string, any>) => Promise<void>;
}
// @public (undocumented)
export interface KibanaExecutionContext {
readonly description: string;
readonly id: string;
readonly name: string;
readonly type: string;
readonly url?: string;
}
// @public
export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Method extends RouteMethod = any> {
// @internal (undocumented)
@ -1374,6 +1404,12 @@ export const kibanaResponseFactory: {
noContent: (options?: HttpResponseOptions) => KibanaResponse<undefined>;
};
// @public (undocumented)
export interface KibanaServerExecutionContext extends Partial<KibanaExecutionContext> {
// (undocumented)
requestId: string;
}
// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts
//
// @public

View file

@ -31,6 +31,7 @@ import { CapabilitiesService } from './capabilities';
import { EnvironmentService, config as pidConfig } from './environment';
// do not try to shorten the import to `./status`, it will break server test mocking
import { StatusService } from './status/status_service';
import { ExecutionContextService } from './execution_context';
import { config as cspConfig } from './csp';
import { config as elasticsearchConfig } from './elasticsearch';
@ -48,6 +49,7 @@ import { CoreUsageDataService } from './core_usage_data';
import { DeprecationsService } from './deprecations';
import { CoreRouteHandlerContext } from './core_route_handler_context';
import { config as externalUrlConfig } from './external_url';
import { config as executionContextConfig } from './execution_context';
const coreId = Symbol('core');
const rootConfigPath = '';
@ -73,6 +75,7 @@ export class Server {
private readonly coreUsageData: CoreUsageDataService;
private readonly i18n: I18nService;
private readonly deprecations: DeprecationsService;
private readonly executionContext: ExecutionContextService;
private readonly savedObjectsStartPromise: Promise<SavedObjectsServiceStart>;
private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void;
@ -109,6 +112,7 @@ export class Server {
this.coreUsageData = new CoreUsageDataService(core);
this.i18n = new I18nService(core);
this.deprecations = new DeprecationsService(core);
this.executionContext = new ExecutionContextService(core);
this.savedObjectsStartPromise = new Promise((resolve) => {
this.resolveSavedObjectsStartPromise = resolve;
@ -133,9 +137,11 @@ export class Server {
const contextServiceSetup = this.context.setup({
pluginDependencies: new Map([...pluginTree.asOpaqueIds]),
});
const executionContextSetup = this.executionContext.setup();
const httpSetup = await this.http.setup({
context: contextServiceSetup,
executionContext: executionContextSetup,
});
// setup i18n prior to any other service, to have translations ready
@ -145,6 +151,7 @@ export class Server {
const elasticsearchServiceSetup = await this.elasticsearch.setup({
http: httpSetup,
executionContext: executionContextSetup,
});
const metricsSetup = await this.metrics.setup({ http: httpSetup });
@ -200,6 +207,7 @@ export class Server {
context: contextServiceSetup,
elasticsearch: elasticsearchServiceSetup,
environment: environmentSetup,
executionContext: executionContextSetup,
http: httpSetup,
i18n: i18nServiceSetup,
savedObjects: savedObjectsSetup,
@ -230,6 +238,7 @@ export class Server {
this.log.debug('starting server');
const startTransaction = apm.startTransaction('server_start', 'kibana_platform');
const executionContextStart = this.executionContext.start();
const elasticsearchStart = await this.elasticsearch.start();
const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration');
const savedObjectsStart = await this.savedObjects.start({
@ -253,6 +262,7 @@ export class Server {
this.coreStart = {
capabilities: capabilitiesStart,
elasticsearch: elasticsearchStart,
executionContext: executionContextStart,
http: httpStart,
metrics: metricsStart,
savedObjects: savedObjectsStart,
@ -297,6 +307,7 @@ export class Server {
public setupCoreConfig() {
const configDescriptors: Array<ServiceConfigDescriptor<unknown>> = [
executionContextConfig,
pathConfig,
cspConfig,
elasticsearchConfig,

View file

@ -20,6 +20,7 @@ import { HttpService, InternalHttpServiceSetup } from '../../../http';
import { registerStatusRoute } from '../status';
import { ServiceStatus, ServiceStatusLevels } from '../../types';
import { statusServiceMock } from '../../status_service.mock';
import { executionContextServiceMock } from '../../../execution_context/execution_context_service.mock';
const coreId = Symbol('core');
@ -35,6 +36,7 @@ describe('GET /api/status', () => {
server = createHttpServer(coreContext);
httpSetup = await server.setup({
context: contextService.setup({ pluginDependencies: new Map() }),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
metrics = metricsServiceMock.createSetupContract();

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/** @public */
export interface KibanaExecutionContext {
/**
* Kibana application initated an operation.
* Can be narrowed to an enum later.
* */
readonly type: string; // 'visualization' | 'actions' | 'server' | ..;
/** public name of a user-facing feature */
readonly name: string; // 'TSVB' | 'Lens' | 'action_execution' | ..;
/** unique value to identify the source */
readonly id: string;
/** human readable description. For example, a vis title, action name */
readonly description: string;
/** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */
readonly url?: string;
}

View file

@ -16,3 +16,4 @@ export * from './app_category';
export * from './ui_settings';
export * from './saved_objects';
export * from './serializable';
export type { KibanaExecutionContext } from './execution_context';

View file

@ -18,6 +18,7 @@ import {
contextServiceMock,
loggingSystemMock,
metricsServiceMock,
executionContextServiceMock,
} from '../../../../../core/server/mocks';
import { createHttpServer } from '../../../../../core/server/test_utils';
import { registerStatsRoute } from '../stats';
@ -37,6 +38,7 @@ describe('/api/stats', () => {
server = createHttpServer();
httpSetup = await server.setup({
context: contextServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
overallStatus$ = new BehaviorSubject<ServiceStatus>({
level: ServiceStatusLevels.available,

View file

@ -0,0 +1,8 @@
{
"id": "corePluginExecutionContext",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["core_plugin_execution_context"],
"server": true,
"ui": false
}

View file

@ -0,0 +1,13 @@
{
"name": "core_plugin_execution_context",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/core_plugin_execution_context",
"kibana": {
"version": "kibana"
},
"license": "SSPL-1.0 OR Elastic License 2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
}
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CorePluginExecutionContext } from './plugin';
export const plugin = () => new CorePluginExecutionContext();

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Plugin, CoreSetup } from 'kibana/server';
export class CorePluginExecutionContext implements Plugin {
public setup(core: CoreSetup, deps: {}) {
const router = core.http.createRouter();
router.get(
{
path: '/execution_context/pass',
validate: false,
options: {
authRequired: false,
},
},
async (context, request, response) => {
const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
return response.ok({ body: headers || {} });
}
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"server/**/*.ts",
],
"exclude": [],
"references": [
{ "path": "../../../../src/core/tsconfig.json" }
]
}

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../services';
import '../../../../test/plugin_functional/plugins/core_provider_plugin/types';
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
describe('execution context', function () {
describe('passed for a client-side operation', () => {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
before(async () => {
await PageObjects.common.navigateToApp('home');
});
it('passes plugin-specific execution context to Elasticsearch server', async () => {
expect(
await browser.execute(async () => {
const coreStart = window._coreProvider.start.core;
const context = coreStart.executionContext.create({
type: 'execution_context_app',
name: 'Execution context app',
id: '42',
// add a non-ASCII symbols to make sure it doesn't break the context propagation mechanism
description: 'какое-то странное описание',
});
const result = await coreStart.http.get('/execution_context/pass', {
context,
});
return result['x-opaque-id'];
})
).to.contain('kibana:execution_context_app:42');
});
});
});
}

View file

@ -12,6 +12,7 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
describe('core plugins', () => {
loadTestFile(require.resolve('./applications'));
loadTestFile(require.resolve('./elasticsearch_client'));
loadTestFile(require.resolve('./execution_context'));
loadTestFile(require.resolve('./server_plugins'));
loadTestFile(require.resolve('./ui_plugins'));
loadTestFile(require.resolve('./ui_settings'));

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { contextServiceMock } from 'src/core/server/mocks';
import {
contextServiceMock,
executionContextServiceMock,
} from '../../../../../../../../src/core/server/mocks';
import { createHttpServer } from 'src/core/server/test_utils';
import supertest from 'supertest';
import { createApmEventClient } from '.';
@ -23,6 +26,7 @@ describe('createApmEventClient', () => {
it('cancels a search when a request is aborted', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
const router = createRouter('/');

View file

@ -14,6 +14,7 @@ import {
contextServiceMock,
elasticsearchServiceMock,
savedObjectsServiceMock,
executionContextServiceMock,
} from '../../../../../src/core/server/mocks';
import { createHttpServer } from '../../../../../src/core/server/test_utils';
import { registerSettingsRoute } from './settings';
@ -48,6 +49,7 @@ describe('/api/settings', () => {
},
},
}),
executionContext: executionContextServiceMock.createInternalSetupContract(),
});
overallStatus$ = new BehaviorSubject<ServiceStatus>({