diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md b/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md
new file mode 100644
index 0000000000000..66c5f3efa2d84
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md)
+
+## CoreStart.executionContext property
+
+[ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md)
+
+Signature:
+
+```typescript
+executionContext: ExecutionContextServiceStart;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md
index 6ad9adca53ef5..df1929b1f20ab 100644
--- a/docs/development/core/public/kibana-plugin-core-public.corestart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md
@@ -20,6 +20,7 @@ export interface CoreStart
| [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart
| [ChromeStart](./kibana-plugin-core-public.chromestart.md) |
| [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart
| [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) |
| [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart
| [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) |
+| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | ExecutionContextServiceStart
| [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) |
| [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart
| [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) |
| [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart
| [HttpStart](./kibana-plugin-core-public.httpstart.md) |
| [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart
| [I18nStart](./kibana-plugin-core-public.i18nstart.md) |
diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md
new file mode 100644
index 0000000000000..b36f8ade848e5
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) > [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 });
+
+```
+
+Signature:
+
+```typescript
+create: (context: KibanaExecutionContext) => IExecutionContextContainer;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md
new file mode 100644
index 0000000000000..d3eecf601ba9c
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md)
+
+## ExecutionContextServiceStart interface
+
+
+Signature:
+
+```typescript
+export interface ExecutionContextServiceStart
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) | (context: KibanaExecutionContext) => IExecutionContextContainer
| 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 });
+
+```
+ |
+
diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md
new file mode 100644
index 0000000000000..6c6ce3171aaeb
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) > [context](./kibana-plugin-core-public.httpfetchoptions.context.md)
+
+## HttpFetchOptions.context property
+
+Signature:
+
+```typescript
+context?: IExecutionContextContainer;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md
index 745020bb60714..020a941189013 100644
--- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md
+++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md
@@ -18,6 +18,7 @@ export interface HttpFetchOptions extends HttpRequestInit
| --- | --- | --- |
| [asResponse](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | boolean
| When true
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 false
, the return type will just be the parsed response body. Defaults to false
. |
| [asSystemRequest](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | boolean
| 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 false
. |
+| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | IExecutionContextContainer
| |
| [headers](./kibana-plugin-core-public.httpfetchoptions.headers.md) | HttpHeadersInit
| Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md). |
| [prependBasePath](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | boolean
| Whether or not the request should automatically prepend the basePath. Defaults to true
. |
| [query](./kibana-plugin-core-public.httpfetchoptions.query.md) | HttpFetchQuery
| The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md). |
diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md
new file mode 100644
index 0000000000000..413b4aaf46b50
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md)
+
+## IExecutionContextContainer interface
+
+
+Signature:
+
+```typescript
+export interface IExecutionContextContainer
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) | () => Record<string, string>
| |
+
diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md
new file mode 100644
index 0000000000000..03132d24bcca5
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md)
+
+## IExecutionContextContainer.toHeader property
+
+Signature:
+
+```typescript
+toHeader: () => Record;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md
new file mode 100644
index 0000000000000..ea8c543c6789e
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md)
+
+## KibanaExecutionContext.description property
+
+human readable description. For example, a vis title, action name
+
+Signature:
+
+```typescript
+readonly description: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md
new file mode 100644
index 0000000000000..1b50d29094585
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md)
+
+## KibanaExecutionContext.id property
+
+unique value to indentify find the source
+
+Signature:
+
+```typescript
+readonly id: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md
new file mode 100644
index 0000000000000..41724f4914264
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md)
+
+## KibanaExecutionContext interface
+
+
+Signature:
+
+```typescript
+export interface KibanaExecutionContext
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) | string
| human readable description. For example, a vis title, action name |
+| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | string
| unique value to indentify find the source |
+| [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) | string
| public name of a user-facing feature |
+| [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) | string
| Kibana application initated an operation. Can be narrowed to an enum later. |
+| [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) | string
| in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url |
+
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md
new file mode 100644
index 0000000000000..21dde32e21ce7
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md)
+
+## KibanaExecutionContext.name property
+
+public name of a user-facing feature
+
+Signature:
+
+```typescript
+readonly name: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md
new file mode 100644
index 0000000000000..ca339ddd9d646
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md)
+
+## KibanaExecutionContext.type property
+
+Kibana application initated an operation. Can be narrowed to an enum later.
+
+Signature:
+
+```typescript
+readonly type: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md
new file mode 100644
index 0000000000000..47ad7604b473d
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [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
+
+Signature:
+
+```typescript
+readonly url?: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md
index a13438ff48e0b..d743508e046ea 100644
--- a/docs/development/core/public/kibana-plugin-core-public.md
+++ b/docs/development/core/public/kibana-plugin-core-public.md
@@ -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 message
and stack
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) | |
diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.executioncontext.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.executioncontext.md
new file mode 100644
index 0000000000000..847b353aee44f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.executioncontext.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md)
+
+## CoreSetup.executionContext property
+
+[ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md)
+
+Signature:
+
+```typescript
+executionContext: ExecutionContextSetup;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md
index b37ac80db87d6..a66db46adf0f7 100644
--- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md
+++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md
@@ -20,6 +20,7 @@ export interface CoreSetupContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) |
| [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) | DeprecationsServiceSetup
| [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) |
| [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup
| [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) |
+| [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md) | ExecutionContextSetup
| [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) |
| [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart>
| [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) |
| [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
}
| [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup
| [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) |
diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.executioncontext.md b/docs/development/core/server/kibana-plugin-core-server.corestart.executioncontext.md
new file mode 100644
index 0000000000000..e58f4dc4afa32
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.corestart.executioncontext.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md)
+
+## CoreStart.executionContext property
+
+[ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md)
+
+Signature:
+
+```typescript
+executionContext: ExecutionContextStart;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md
index f98088648689f..d7aaba9149cf5 100644
--- a/docs/development/core/server/kibana-plugin-core-server.corestart.md
+++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md
@@ -18,6 +18,7 @@ export interface CoreStart
| --- | --- | --- |
| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart
| [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) |
| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart
| [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) |
+| [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md) | ExecutionContextStart
| [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) |
| [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart
| [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) |
| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart
| [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) |
| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart
| [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) |
diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md
new file mode 100644
index 0000000000000..d152b9a0c5df2
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [get](./kibana-plugin-core-server.executioncontextsetup.get.md)
+
+## ExecutionContextSetup.get() method
+
+Retrieves an opearation meta-data for the current async context.
+
+Signature:
+
+```typescript
+get(): IExecutionContextContainer | undefined;
+```
+Returns:
+
+`IExecutionContextContainer | undefined`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md
new file mode 100644
index 0000000000000..137df77769c8d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md)
+
+## ExecutionContextSetup interface
+
+
+Signature:
+
+```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. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md
new file mode 100644
index 0000000000000..4c8ba4d21b8c4
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [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.
+
+Signature:
+
+```typescript
+set(context: Partial): void;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| context | Partial<KibanaServerExecutionContext>
| |
+
+Returns:
+
+`void`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextstart.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextstart.md
new file mode 100644
index 0000000000000..115c09471b3f7
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextstart.md
@@ -0,0 +1,12 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md)
+
+## ExecutionContextStart type
+
+
+Signature:
+
+```typescript
+export declare type ExecutionContextStart = ExecutionContextSetup;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.md
new file mode 100644
index 0000000000000..2ab3f52b9b553
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md)
+
+## IExecutionContextContainer interface
+
+
+Signature:
+
+```typescript
+export interface IExecutionContextContainer
+```
+
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [toJSON()](./kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md) | |
+| [toString()](./kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md) | |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md
new file mode 100644
index 0000000000000..f67aa88862fee
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md) > [toJSON](./kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md)
+
+## IExecutionContextContainer.toJSON() method
+
+Signature:
+
+```typescript
+toJSON(): Readonly;
+```
+Returns:
+
+`Readonly`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md
new file mode 100644
index 0000000000000..60f9f499cf36c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExecutionContextContainer](./kibana-plugin-core-server.iexecutioncontextcontainer.md) > [toString](./kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md)
+
+## IExecutionContextContainer.toString() method
+
+Signature:
+
+```typescript
+toString(): string;
+```
+Returns:
+
+`string`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md
new file mode 100644
index 0000000000000..00c907b578cf3
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md)
+
+## KibanaExecutionContext.description property
+
+human readable description. For example, a vis title, action name
+
+Signature:
+
+```typescript
+readonly description: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md
new file mode 100644
index 0000000000000..d86f621231214
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md)
+
+## KibanaExecutionContext.id property
+
+unique value to indentify find the source
+
+Signature:
+
+```typescript
+readonly id: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md
new file mode 100644
index 0000000000000..ebc2aeb419a75
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md)
+
+## KibanaExecutionContext interface
+
+
+Signature:
+
+```typescript
+export interface KibanaExecutionContext
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) | string
| human readable description. For example, a vis title, action name |
+| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | string
| unique value to indentify find the source |
+| [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) | string
| public name of a user-facing feature |
+| [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) | string
| Kibana application initated an operation. Can be narrowed to an enum later. |
+| [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) | string
| in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md
new file mode 100644
index 0000000000000..92f58c01bcc11
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md)
+
+## KibanaExecutionContext.name property
+
+public name of a user-facing feature
+
+Signature:
+
+```typescript
+readonly name: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md
new file mode 100644
index 0000000000000..534b0cdea1753
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md)
+
+## KibanaExecutionContext.type property
+
+Kibana application initated an operation. Can be narrowed to an enum later.
+
+Signature:
+
+```typescript
+readonly type: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md
new file mode 100644
index 0000000000000..dee241cd79398
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [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
+
+Signature:
+
+```typescript
+readonly url?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md
new file mode 100644
index 0000000000000..f309e4fd0006c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md)
+
+## KibanaServerExecutionContext interface
+
+
+Signature:
+
+```typescript
+export interface KibanaServerExecutionContext extends Partial
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) | string
| |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md
new file mode 100644
index 0000000000000..dff3fd7f2e9ff
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) > [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md)
+
+## KibanaServerExecutionContext.requestId property
+
+Signature:
+
+```typescript
+requestId: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index ac8930c52ac5c..4a203f10e7cd3 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -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 asCurrentUser
method that doesn't use credentials of the Kibana internal user (as asInternalUser
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 auth
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. |
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index d9a48835553cf..de495b20344f6 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -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, <> must also be set to `false.`
diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts
index afb8aec31cccd..c80c2e3f49775 100644
--- a/src/core/public/core_system.test.mocks.ts
+++ b/src/core/public/core_system.test.mocks.ts
@@ -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', () => ({
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 8ead0f50785bd..efafb25da27ee 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -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({
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index e5dcd8f817a0a..43e7d443f5c00 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -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 = '';
}
}
diff --git a/src/core/public/execution_context/execution_context_container.test.ts b/src/core/public/execution_context/execution_context_container.test.ts
new file mode 100644
index 0000000000000..a4ee355ab40a4
--- /dev/null
+++ b/src/core/public/execution_context/execution_context_container.test.ts
@@ -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",
+ }
+ `);
+ });
+ });
+});
diff --git a/src/core/public/execution_context/execution_context_container.ts b/src/core/public/execution_context/execution_context_container.ts
new file mode 100644
index 0000000000000..9c8e3e269ec88
--- /dev/null
+++ b/src/core/public/execution_context/execution_context_container.ts
@@ -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;
+}
+
+export class ExecutionContextContainer implements IExecutionContextContainer {
+ readonly #context: Readonly;
+ constructor(context: Readonly) {
+ 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() };
+ }
+}
diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts
new file mode 100644
index 0000000000000..d8148b0af807d
--- /dev/null
+++ b/src/core/public/execution_context/execution_context_service.mock.ts
@@ -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> = {
+ toHeader: jest.fn(),
+ };
+ return mock;
+};
+const createStartContractMock = () => {
+ const mock: jest.Mocked = {
+ create: jest.fn().mockReturnValue(createContainerMock()),
+ };
+ return mock;
+};
+
+const createMock = (): jest.Mocked => ({
+ setup: jest.fn(),
+ start: jest.fn(),
+ stop: jest.fn(),
+});
+
+export const executionContextServiceMock = {
+ create: createMock,
+ createStartContract: createStartContractMock,
+ createContainer: createContainerMock,
+};
diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts
new file mode 100644
index 0000000000000..934e68d15be04
--- /dev/null
+++ b/src/core/public/execution_context/execution_context_service.ts
@@ -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 {
+ setup() {}
+ start(): ExecutionContextServiceStart {
+ return {
+ create(context: KibanaExecutionContext) {
+ return new ExecutionContextContainer(context);
+ },
+ };
+ }
+ stop() {}
+}
diff --git a/src/core/public/execution_context/index.ts b/src/core/public/execution_context/index.ts
new file mode 100644
index 0000000000000..d0c8348d864e7
--- /dev/null
+++ b/src/core/public/execution_context/index.ts
@@ -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';
diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts
index 1208df032ff6f..67ec816d08430 100644
--- a/src/core/public/http/fetch.test.ts
+++ b/src/core/public/http/fetch.test.ts
@@ -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(duration: number) {
return new Promise((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('*', {});
diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts
index 87df54f2c6a8a..fb178a937e18a 100644
--- a/src/core/public/http/fetch.ts
+++ b/src/core/public/http/fetch.ts
@@ -124,6 +124,7 @@ export class Fetch {
'Content-Type': 'application/json',
...options.headers,
'kbn-version': this.params.kibanaVersion,
+ ...options.context?.toHeader(),
}),
};
diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts
index 73418eafccd71..ccf68201bc207 100644
--- a/src/core/public/http/types.ts
+++ b/src/core/public/http/types.ts
@@ -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;
}
/**
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 9bf1a05abc34e..b3dd3827352bd 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -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
diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts
index bd7623beba651..63b94ea4ac4e3 100644
--- a/src/core/public/mocks.ts
+++ b/src/core/public/mocks.ts
@@ -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;
diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts
index 49c895aa80fc4..be3cff54aca8e 100644
--- a/src/core/public/plugins/plugin_context.ts
+++ b/src/core/public/plugins/plugin_context.ts
@@ -140,5 +140,6 @@ export function createPluginStartContext<
},
fatalErrors: deps.fatalErrors,
deprecations: deps.deprecations,
+ executionContext: deps.executionContext,
};
}
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index d7114f14e2f00..d62a4bcdd1e51 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -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;
@@ -103,6 +104,7 @@ describe('PluginsService', () => {
savedObjects: savedObjectsServiceMock.createStartContract(),
fatalErrors: fatalErrorsServiceMock.createStartContract(),
deprecations: deprecationsServiceMock.createStartContract(),
+ executionContext: executionContextServiceMock.createStartContract(),
};
mockStartContext = {
...mockStartDeps,
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index f18dfb02fd41d..d23980ff55a2c 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -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;
+}
+
// @public
export interface IExternalUrl {
validateUrl(relativeOrAbsoluteUrl: string): URL | null;
@@ -949,6 +964,15 @@ export interface IUiSettingsClient {
set: (key: string, value: any) => Promise;
}
+// @public (undocumented)
+export interface KibanaExecutionContext {
+ readonly description: string;
+ readonly id: string;
+ readonly name: string;
+ readonly type: string;
+ readonly url?: string;
+}
+
// @public
export type MountPoint = (element: T) => UnmountCallback;
@@ -1663,6 +1687,6 @@ export interface UserProvidedValues {
// 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
```
diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
index 4e4bc4a51a7a8..ac793d960d03b 100644
--- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
+++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
@@ -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,
diff --git a/src/core/server/core_app/integration_tests/bundle_routes.test.ts b/src/core/server/core_app/integration_tests/bundle_routes.test.ts
index fbe2e9285ba29..7c50e09b12468 100644
--- a/src/core/server/core_app/integration_tests/bundle_routes.test.ts
+++ b/src/core/server/core_app/integration_tests/bundle_routes.test.ts
@@ -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 });
diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts
index befad222030f9..f96f39349887e 100644
--- a/src/core/server/elasticsearch/client/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/client/cluster_client.test.ts
@@ -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,
});
});
diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts
index 278bc6f3b0a65..d164736cead07 100644
--- a/src/core/server/elasticsearch/client/cluster_client.ts
+++ b/src/core/server/elasticsearch/client/cluster_client.ts
@@ -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);
}
diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts
index 2c3a203dbc49d..924f1584c5f8f 100644
--- a/src/core/server/elasticsearch/client/configure_client.test.ts
+++ b/src/core/server/elasticsearch/client/configure_client.test.ts
@@ -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);
});
diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts
index ae1a2f67b74c8..ce4bd6fa2c59b 100644
--- a/src/core/server/elasticsearch/client/configure_client.ts
+++ b/src/core/server/elasticsearch/client/configure_client.ts
@@ -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;
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 8a6da8d251e37..791ae2ab7abaa 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -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);
diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts
index 7da83145ccd42..deb2d49f70817 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.ts
@@ -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()
);
}
diff --git a/src/core/server/execution_context/execution_context_config.ts b/src/core/server/execution_context/execution_context_config.ts
new file mode 100644
index 0000000000000..af6e7253433f7
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_config.ts
@@ -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;
+
+export const config: ServiceConfigDescriptor = {
+ path: 'execution_context',
+ schema: configSchema,
+};
diff --git a/src/core/server/execution_context/execution_context_container.test.ts b/src/core/server/execution_context/execution_context_container.test.ts
new file mode 100644
index 0000000000000..46a688c8abdf0
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_container.test.ts
@@ -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();
+ });
+});
diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts
new file mode 100644
index 0000000000000..71bf4bb96e1b0
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_container.ts
@@ -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
+): 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;
+}
+
+export class ExecutionContextContainer implements IExecutionContextContainer {
+ readonly #context: Readonly;
+ constructor(context: Readonly) {
+ 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 {
+ return this.#context;
+ }
+}
diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts
new file mode 100644
index 0000000000000..657805df273ca
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_service.mock.ts
@@ -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 = {
+ set: jest.fn(),
+ reset: jest.fn(),
+ get: jest.fn(),
+ getParentContextFrom: jest.fn(),
+ };
+ return mock;
+};
+const createInternalSetupContractMock = () => {
+ const setupContract: jest.Mocked = createExecutionContextMock();
+ return setupContract;
+};
+
+const createSetupContractMock = () => {
+ const mock: jest.Mocked = {
+ set: jest.fn(),
+ get: jest.fn(),
+ };
+ return mock;
+};
+
+export const executionContextServiceMock = {
+ createInternalSetupContract: createInternalSetupContractMock,
+ createInternalStartContract: createInternalSetupContractMock,
+ createSetupContract: createSetupContractMock,
+ createStartContract: createSetupContractMock,
+};
diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts
new file mode 100644
index 0000000000000..9b9ab0f48bacb
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_service.test.ts
@@ -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();
+ });
+ });
+});
diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts
new file mode 100644
index 0000000000000..95a854f84d145
--- /dev/null
+++ b/src/core/server/execution_context/execution_context_service.ts
@@ -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 {
+ requestId: string;
+}
+
+/**
+ * @internal
+ */
+export interface IExecutionContext {
+ getParentContextFrom(headers: Record): KibanaExecutionContext | undefined;
+ set(context: Partial): 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): void;
+ /**
+ * Retrieves an opearation meta-data for the current async context.
+ **/
+ get(): IExecutionContextContainer | undefined;
+}
+
+/**
+ * @public
+ */
+export type ExecutionContextStart = ExecutionContextSetup;
+
+export class ExecutionContextService
+ implements CoreService {
+ private readonly log: Logger;
+ private readonly asyncLocalStorage: AsyncLocalStorage;
+ private enabled = false;
+ private configSubscription?: Subscription;
+
+ constructor(private readonly coreContext: CoreContext) {
+ this.log = coreContext.logger.get('execution_context');
+ this.asyncLocalStorage = new AsyncLocalStorage();
+ }
+
+ setup(): InternalExecutionContextSetup {
+ this.configSubscription = this.coreContext.configService
+ .atPath('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();
+ }
+}
diff --git a/src/core/server/execution_context/index.ts b/src/core/server/execution_context/index.ts
new file mode 100644
index 0000000000000..f8018c75995e7
--- /dev/null
+++ b/src/core/server/execution_context/index.ts
@@ -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';
diff --git a/src/core/server/execution_context/integration_tests/tracing.test.ts b/src/core/server/execution_context/integration_tests/tracing.test.ts
new file mode 100644
index 0000000000000..c9de5fb98eb02
--- /dev/null
+++ b/src/core/server/execution_context/integration_tests/tracing.test.ts
@@ -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;
+ 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;
+ 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');
+ });
+ });
+});
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index 55af02a08561b..b09b200620fbf 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -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) => {
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index d43d86d587d06..85c035154a7a7 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -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 {
+ public async setup(
+ config: HttpConfig,
+ executionContext?: InternalExecutionContextSetup
+ ): Promise {
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;
diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts
index ebb9ad971b848..d8a7b54275480 100644
--- a/src/core/server/http/http_service.test.ts
+++ b/src/core/server/http/http_service.test.ts
@@ -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,
diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts
index 0d28506607682..0097aab82b21c 100644
--- a/src/core/server/http/http_service.ts
+++ b/src/core/server/http/http_service.ts
@@ -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);
diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts
index 6897160951aab..da8abe55b6592 100644
--- a/src/core/server/http/integration_tests/lifecycle.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle.test.ts
@@ -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(() => {
diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
index c2023c5577d61..077e2f6e9c485 100644
--- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
@@ -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', () => {
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index dfc47098724cc..ecacbf0bfa0c2 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -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(() => {
diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts
index 354ab1c65d565..1b2b0b966d3a2 100644
--- a/src/core/server/http/integration_tests/router.test.ts
+++ b/src/core/server/http/integration_tests/router.test.ts
@@ -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(() => {
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 77946e15ef686..d2a4b4bff3390 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -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 {
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);
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 0d52ff64499c1..ff844f44aede0 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -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 = jest.Mocked['config']>;
@@ -144,6 +146,7 @@ function createCoreSetupMock({
logging: loggingServiceMock.createSetupContract(),
metrics: metricsServiceMock.createSetupContract(),
deprecations: deprecationsServiceMock.createSetupContract(),
+ executionContext: executionContextServiceMock.createInternalSetupContract(),
getStartServices: jest
.fn, 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;
}
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index c466eb2b9ee09..70fd1c60efa61 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -115,6 +115,7 @@ export function createPluginSetupContext(
elasticsearch: {
legacy: deps.elasticsearch.legacy,
},
+ executionContext: deps.executionContext,
http: {
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
registerRouteHandlerContext: <
@@ -195,6 +196,7 @@ export function createPluginStartContext(
createClient: deps.elasticsearch.createClient,
legacy: deps.elasticsearch.legacy,
},
+ executionContext: deps.executionContext,
http: {
auth: deps.http.auth,
basePath: deps.http.basePath,
diff --git a/src/core/server/saved_objects/routes/integration_tests/get.test.ts b/src/core/server/saved_objects/routes/integration_tests/get.test.ts
index 295f80712b765..e247a913f9779 100644
--- a/src/core/server/saved_objects/routes/integration_tests/get.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/get.test.ts
@@ -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();
diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
index 96d79edd39d3d..294267a0e2ae7 100644
--- a/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
@@ -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();
diff --git a/src/core/server/saved_objects/routes/test_utils.ts b/src/core/server/saved_objects/routes/test_utils.ts
index e6826b118509e..796bfd55b7827 100644
--- a/src/core/server/saved_objects/routes/test_utils.ts
+++ b/src/core/server/saved_objects/routes/test_utils.ts
@@ -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();
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 13ec594df9075..ed55c6e3d09cb 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -523,6 +523,8 @@ export interface CoreSetup;
// (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): void;
+}
+
+// @public (undocumented)
+export type ExecutionContextStart = ExecutionContextSetup;
+
// @public
export interface FakeRequest {
headers: Headers;
@@ -1187,6 +1200,14 @@ export interface ICustomClusterClient extends IClusterClient {
close: () => Promise;
}
+// @public (undocumented)
+export interface IExecutionContextContainer {
+ // (undocumented)
+ toJSON(): Readonly;
+ // (undocumented)
+ toString(): string;
+}
+
// @public
export interface IExternalUrlConfig {
readonly policy: IExternalUrlPolicy[];
@@ -1303,6 +1324,15 @@ export interface IUiSettingsClient {
setMany: (changes: Record) => Promise;
}
+// @public (undocumented)
+export interface KibanaExecutionContext {
+ readonly description: string;
+ readonly id: string;
+ readonly name: string;
+ readonly type: string;
+ readonly url?: string;
+}
+
// @public
export class KibanaRequest {
// @internal (undocumented)
@@ -1374,6 +1404,12 @@ export const kibanaResponseFactory: {
noContent: (options?: HttpResponseOptions) => KibanaResponse;
};
+// @public (undocumented)
+export interface KibanaServerExecutionContext extends Partial {
+ // (undocumented)
+ requestId: string;
+}
+
// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts
//
// @public
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 3f553dd90678e..5a75550280a96 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -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;
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> = [
+ executionContextConfig,
pathConfig,
cspConfig,
elasticsearchConfig,
diff --git a/src/core/server/status/routes/integration_tests/status.test.ts b/src/core/server/status/routes/integration_tests/status.test.ts
index 841dc43cc4ee3..03b9a1462e0c3 100644
--- a/src/core/server/status/routes/integration_tests/status.test.ts
+++ b/src/core/server/status/routes/integration_tests/status.test.ts
@@ -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();
diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts
new file mode 100644
index 0000000000000..e624ea82f22fc
--- /dev/null
+++ b/src/core/types/execution_context.ts
@@ -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;
+}
diff --git a/src/core/types/index.ts b/src/core/types/index.ts
index 21876844ed45b..97f990f608c04 100644
--- a/src/core/types/index.ts
+++ b/src/core/types/index.ts
@@ -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';
diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
index 1409e4dd2c908..8466093664d0d 100644
--- a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
+++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
@@ -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({
level: ServiceStatusLevels.available,
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json b/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json
new file mode 100644
index 0000000000000..625745202e39b
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "corePluginExecutionContext",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["core_plugin_execution_context"],
+ "server": true,
+ "ui": false
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/package.json b/test/plugin_functional/plugins/core_plugin_execution_context/package.json
new file mode 100644
index 0000000000000..4b932850cfa04
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/package.json
@@ -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"
+ }
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/server/index.ts b/test/plugin_functional/plugins/core_plugin_execution_context/server/index.ts
new file mode 100644
index 0000000000000..019e302096752
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/server/index.ts
@@ -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();
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts
new file mode 100644
index 0000000000000..48889c6d4a455
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts
@@ -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() {}
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json
new file mode 100644
index 0000000000000..21662b2b64a18
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json
@@ -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" }
+ ]
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/execution_context.ts b/test/plugin_functional/test_suites/core_plugins/execution_context.ts
new file mode 100644
index 0000000000000..21bcddd32bc19
--- /dev/null
+++ b/test/plugin_functional/test_suites/core_plugins/execution_context.ts
@@ -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');
+ });
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts
index 87a153a24570d..79850dd633375 100644
--- a/test/plugin_functional/test_suites/core_plugins/index.ts
+++ b/test/plugin_functional/test_suites/core_plugins/index.ts
@@ -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'));
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
index 8e82a189d75f3..a98bdab53cad9 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
@@ -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('/');
diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts
index 2034a4e5b74ba..57dcc924375cd 100644
--- a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts
+++ b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts
@@ -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({