mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[examples] add routes to access v8 profiling (#155956)
Adds routes to run v8 profiling tools, when running the examples plugins via `--run-examples` See the included README.md for more info
This commit is contained in:
parent
b12238bac8
commit
1f3426942c
17 changed files with 547 additions and 0 deletions
112
examples/v8_profiler_examples/README.md
Normal file
112
examples/v8_profiler_examples/README.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
# V8 profiler examples
|
||||
|
||||
Provides access to the V8 CPU Profiler and Heap Profiler, to inspect
|
||||
the Kibana running this plugin.
|
||||
|
||||
The endpoints are:
|
||||
|
||||
/_dev/cpu_profile?duration=(seconds)&interval=(microseconds)
|
||||
/_dev/heap_profile?duration=(seconds)&interval=(bytes)
|
||||
|
||||
The default duration is 5 seconds.
|
||||
|
||||
For more information on these V8 APIs, see:
|
||||
|
||||
- https://chromedevtools.github.io/devtools-protocol/v8/Profiler/
|
||||
- https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler/
|
||||
|
||||
Note that due to bugs or limitations,
|
||||
it's not possible to generate heap snapshots using the techniques used
|
||||
to generate the cpu and heap profiles.
|
||||
|
||||
Try them right now, assuming you started Kibana with `--run-examples`!
|
||||
|
||||
- [`http://localhost:5601/_dev/cpu_profile`](http://localhost:5601/_dev/cpu_profile)
|
||||
- [`http://localhost:5601/_dev/heap_profile`](http://localhost:5601/_dev/heap_profile)
|
||||
|
||||
|
||||
When using curl, you can use the `-kOJ` options, which:
|
||||
|
||||
- `-k --insecure`: allow HTTPS usage with self-signed certs
|
||||
- `-O --remote-name`: use the server-specified name for this download
|
||||
- `-J --remote-header-name`: use the `Content-Disposition` as the name of
|
||||
the download
|
||||
|
||||
So one of the following should work for you, to run a 10 second CPU profile
|
||||
using an interval of 100μs (default: 5s / 1000μs):
|
||||
|
||||
```
|
||||
curl -OJ "http://elastic:changeme@localhost:5601/_dev/cpu_profile?duration=10&interval=100"
|
||||
curl -kOJ "https://elastic:changeme@localhost:5601/_dev/cpu_profile?duration=10&interval=100"
|
||||
```
|
||||
|
||||
The files generated will be:
|
||||
|
||||
MM-DD_hh-mm-ss.cpuprofile
|
||||
MM-DD_hh-mm-ss.heapprofile
|
||||
|
||||
These filetypes are the ones expected by various V8 tools that can read these.
|
||||
|
||||
You can use these URLs in your browser, and the files will be saved with the
|
||||
generated names.
|
||||
|
||||
## profile / heap profile readers
|
||||
|
||||
The traditional tools used to view these are part of Chrome Dev Tools (CDT)
|
||||
and now VS Code also supports viewing these files. They provide
|
||||
similar capabilities.
|
||||
|
||||
There doesn't seem to be a much doc available on how to use the viewers
|
||||
for these files. The Chrome Dev Tools docs are extremely old and appear
|
||||
to be out-of-date with the current user interface. The VS Code
|
||||
documentation [Analyzing a profile][] is more recent, but there's not
|
||||
much there.
|
||||
|
||||
[Analyzing a profile]: https://code.visualstudio.com/docs/nodejs/profiling#_analyzing-a-profile
|
||||
|
||||
For CPU profiles, open CDT and then click on the "Performance" tab. You
|
||||
should be able to drop a file right from Finder / Explorer onto the CDT
|
||||
window, and then get the visualization of the profile. If you
|
||||
downloaded the profile right from the browser, using the URL in the URL
|
||||
bar, you can drop the download file from the download status button
|
||||
right into CDT.
|
||||
|
||||
For heap profiles, open CDT and then click on the "Memory" tab. Drag and
|
||||
drop doesn't seem to work here, but you can load a file via a file
|
||||
prompter by clicking the "Load" button at the bottom of the "Memory"
|
||||
pane. You will probably need to scroll to see the button.
|
||||
|
||||
VSCode now supports `.cpuprofile` files and `.heapprofile` files
|
||||
directly, displaying them as a table of function timings. There is also
|
||||
[an extension available to display flame charts][] installed by clicking
|
||||
on the grey-ed out "flame" button on the top-right of the cpu profile
|
||||
view.
|
||||
|
||||
[an extension available to display flame charts]: https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame
|
||||
|
||||
There seem to be problems with both CDT and the VS Code tools, at times.
|
||||
The flame charts in VS Code seem to go haywire sometimes. The heap
|
||||
profile tables in CDT don't expand. Etc. So, beware, and be prepared to
|
||||
have to use multiple tools to analyze these files.
|
||||
|
||||
An alternate view of CPU profiles, which organizes files based on
|
||||
"packages", is available with the **NO**de **PRO**filer (`no-pro`) thing
|
||||
available at https://pmuellr.github.io/no-pro/ . It also supports
|
||||
drag-n-drop of CPU profile files. Note that you can get more
|
||||
directories to show up as "packages", by bringing up CDT and running the
|
||||
following code:
|
||||
|
||||
localStorage['fake-packages-dirs'] = "x-pack/plugins,packages,src/core"
|
||||
|
||||
|
||||
## tips / tricks
|
||||
|
||||
If you're handy with Mac Finder, or other ways of auto-launching apps
|
||||
based on file extensions, it's easy to associate `.cpuprofile` files
|
||||
with vscode.
|
||||
|
||||
Since the http endpoints are GET requests, they are easy to bookmark.
|
||||
Start a profile by clicking on a bookmark.
|
||||
|
||||
When these files are downloaded via Chrome, you can typically launch
|
||||
them directly from the download bar, or drag the file to a viewer.
|
12
examples/v8_profiler_examples/kibana.jsonc
Normal file
12
examples/v8_profiler_examples/kibana.jsonc
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/v8-profiler-examples-plugin",
|
||||
"owner": "@elastic/response-ops",
|
||||
"description": "Provides access to the v8 cpu profiler and heap profiler running this app",
|
||||
"plugin": {
|
||||
// umm, vs code says I can't use digits in the id field?
|
||||
"id": "vEIGHTProfilerExamples",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
}
|
||||
}
|
14
examples/v8_profiler_examples/server/index.ts
Normal file
14
examples/v8_profiler_examples/server/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { V8ProfilerExamplesPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new V8ProfilerExamplesPlugin(initializerContext);
|
||||
}
|
34
examples/v8_profiler_examples/server/lib/cpu_profile.ts
Normal file
34
examples/v8_profiler_examples/server/lib/cpu_profile.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Session } from './session';
|
||||
|
||||
interface StartProfilingArgs {
|
||||
interval: number;
|
||||
}
|
||||
|
||||
// Start a new profile, resolves to a function to stop the profile and resolve
|
||||
// the profile data.
|
||||
export async function startProfiling(
|
||||
session: Session,
|
||||
args: StartProfilingArgs
|
||||
): Promise<() => any> {
|
||||
session.logger.info(`starting cpu profile with args: ${JSON.stringify(args)}`);
|
||||
|
||||
await session.post('Profiler.enable');
|
||||
// microseconds, v8 default is 1000
|
||||
await session.post('Profiler.setSamplingInterval', args);
|
||||
await session.post('Profiler.start');
|
||||
|
||||
// returned function which stops the profile and resolves to the profile data
|
||||
return async function stopProfiling() {
|
||||
session.logger.info('stopping cpu profile');
|
||||
const result: any = await session.post('Profiler.stop');
|
||||
return result.profile;
|
||||
};
|
||||
}
|
27
examples/v8_profiler_examples/server/lib/deferred.ts
Normal file
27
examples/v8_profiler_examples/server/lib/deferred.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 function createDeferred() {
|
||||
let resolver: any;
|
||||
let rejecter: any;
|
||||
|
||||
function resolve(...args: any[]) {
|
||||
resolver(...args);
|
||||
}
|
||||
|
||||
function reject(...args: any[]) {
|
||||
rejecter(...args);
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve_, reject_) => {
|
||||
resolver = resolve_;
|
||||
rejecter = reject_;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
35
examples/v8_profiler_examples/server/lib/heap_profile.ts
Normal file
35
examples/v8_profiler_examples/server/lib/heap_profile.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { Session } from './session';
|
||||
|
||||
interface StartProfilingArgs {
|
||||
samplingInterval: number;
|
||||
includeObjectsCollectedByMajorGC: boolean;
|
||||
includeObjectsCollectedByMinorGC: boolean;
|
||||
}
|
||||
|
||||
// Start a new profile, resolves to a function to stop the profile and resolve
|
||||
// the profile data.
|
||||
export async function startProfiling(
|
||||
session: Session,
|
||||
args: StartProfilingArgs
|
||||
): Promise<() => any> {
|
||||
session.logger.info(`starting heap profile with args: ${JSON.stringify(args)}`);
|
||||
|
||||
await session.post('Profiler.enable');
|
||||
await session.post('HeapProfiler.enable');
|
||||
await session.post('HeapProfiler.startSampling', args);
|
||||
|
||||
// returned function which stops the profile and resolves to the profile data
|
||||
return async function stopProfiling() {
|
||||
session.logger.info('stopping heap profile');
|
||||
const result: any = await session.post('HeapProfiler.stopSampling');
|
||||
return result.profile;
|
||||
};
|
||||
}
|
79
examples/v8_profiler_examples/server/lib/session.ts
Normal file
79
examples/v8_profiler_examples/server/lib/session.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
|
||||
import { createDeferred } from './deferred';
|
||||
|
||||
let inspector: any = null;
|
||||
try {
|
||||
inspector = require('inspector');
|
||||
} catch (err) {
|
||||
// inspector will be null :-(
|
||||
}
|
||||
|
||||
export async function createSession(logger: Logger): Promise<Session> {
|
||||
logger.debug('creating session');
|
||||
|
||||
if (inspector == null) {
|
||||
throw new Error('the inspector module is not available for this version of node');
|
||||
}
|
||||
|
||||
let session = null;
|
||||
try {
|
||||
session = new inspector.Session();
|
||||
} catch (err) {
|
||||
throw new Error(`error creating inspector session: ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
session.connect();
|
||||
} catch (err) {
|
||||
throw new Error(`error connecting inspector session: ${err.message}`);
|
||||
}
|
||||
|
||||
return new Session(logger, session);
|
||||
}
|
||||
|
||||
export class Session {
|
||||
readonly logger: Logger;
|
||||
private session: any;
|
||||
|
||||
constructor(logger: Logger, session: any) {
|
||||
this.logger = logger;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
this.session.disconnect();
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
on(event: string, handler: any) {
|
||||
this.session.on(event, handler);
|
||||
}
|
||||
|
||||
async post(method: string, args?: any) {
|
||||
this.logger.debug(`posting method ${method} ${JSON.stringify(args)}`);
|
||||
if (this.session == null) {
|
||||
throw new Error('session disconnected');
|
||||
}
|
||||
|
||||
const deferred = createDeferred();
|
||||
|
||||
this.session.post(method, args, (err: any, response: any) => {
|
||||
if (err) {
|
||||
this.logger.debug(`error from method ${method}: ${err.message}`);
|
||||
return deferred.reject(err);
|
||||
}
|
||||
deferred.resolve(response);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
29
examples/v8_profiler_examples/server/plugin.ts
Normal file
29
examples/v8_profiler_examples/server/plugin.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { Plugin, Logger, CoreSetup, PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
import { registerRoutes } from './routes';
|
||||
|
||||
// this plugin's dependencies
|
||||
export class V8ProfilerExamplesPlugin implements Plugin<void, void> {
|
||||
readonly logger: Logger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
const router = core.http.createRouter();
|
||||
registerRoutes(this.logger, router);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
84
examples/v8_profiler_examples/server/routes/common.ts
Normal file
84
examples/v8_profiler_examples/server/routes/common.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { Logger, IKibanaResponse, KibanaResponseFactory } from '@kbn/core/server';
|
||||
import { createSession, Session } from '../lib/session';
|
||||
import { createDeferred } from '../lib/deferred';
|
||||
|
||||
type StopProfilingFn = () => Promise<any>;
|
||||
type StartProfilingFn<ArgType> = (session: Session, args: ArgType) => Promise<StopProfilingFn>;
|
||||
|
||||
export async function handleRoute<ArgType>(
|
||||
startProfiling: StartProfilingFn<ArgType>,
|
||||
args: ArgType,
|
||||
logger: Logger,
|
||||
response: KibanaResponseFactory,
|
||||
duration: number,
|
||||
type: string
|
||||
): Promise<IKibanaResponse> {
|
||||
let session: Session;
|
||||
|
||||
try {
|
||||
session = await createSession(logger);
|
||||
} catch (err) {
|
||||
const message = `unable to create session: ${err.message}`;
|
||||
logger.error(message);
|
||||
return response.badRequest({ body: message });
|
||||
}
|
||||
|
||||
const deferred = createDeferred();
|
||||
let stopProfiling: any;
|
||||
try {
|
||||
stopProfiling = await startProfiling(session, args);
|
||||
} catch (err) {
|
||||
const message = `unable to start ${type} profiling: ${err.message}`;
|
||||
logger.error(message);
|
||||
return response.badRequest({ body: message });
|
||||
}
|
||||
|
||||
setTimeout(whenDone, 1000 * duration);
|
||||
|
||||
let profile;
|
||||
async function whenDone() {
|
||||
try {
|
||||
profile = await stopProfiling();
|
||||
} catch (err) {
|
||||
logger.warn(`unable to capture ${type} profile: ${err.message}`);
|
||||
}
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
await deferred.promise;
|
||||
|
||||
try {
|
||||
await session.destroy();
|
||||
} catch (err) {
|
||||
logger.warn(`unable to destroy session: ${err.message}`);
|
||||
}
|
||||
|
||||
if (profile == null) {
|
||||
const message = `unable to capture ${type} profile`;
|
||||
logger.error(message);
|
||||
return response.badRequest({ body: message });
|
||||
}
|
||||
|
||||
const fileName = new Date()
|
||||
.toISOString()
|
||||
.replace('T', '_')
|
||||
.replace(/\//g, '-')
|
||||
.replace(/:/g, '-')
|
||||
.substring(5, 19);
|
||||
|
||||
return response.ok({
|
||||
body: profile,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename="${fileName}.${type}profile"`,
|
||||
},
|
||||
});
|
||||
}
|
37
examples/v8_profiler_examples/server/routes/cpu_profile.ts
Normal file
37
examples/v8_profiler_examples/server/routes/cpu_profile.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { Logger, IRouter } from '@kbn/core/server';
|
||||
import { startProfiling } from '../lib/cpu_profile';
|
||||
import { handleRoute } from './common';
|
||||
|
||||
const routeValidation = {
|
||||
query: schema.object({
|
||||
// seconds to run the profile
|
||||
duration: schema.number({ defaultValue: 5 }),
|
||||
// microseconds, v8 default is 1000
|
||||
interval: schema.number({ defaultValue: 1000 }),
|
||||
}),
|
||||
};
|
||||
|
||||
const routeConfig = {
|
||||
path: '/_dev/cpu_profile',
|
||||
validate: routeValidation,
|
||||
};
|
||||
|
||||
export function registerRoute(logger: Logger, router: IRouter): void {
|
||||
router.get(routeConfig, async (context, request, response) => {
|
||||
const { duration, interval } = request.query;
|
||||
const args = {
|
||||
interval,
|
||||
};
|
||||
|
||||
return await handleRoute(startProfiling, args, logger, response, duration, 'cpu');
|
||||
});
|
||||
}
|
41
examples/v8_profiler_examples/server/routes/heap_profile.ts
Normal file
41
examples/v8_profiler_examples/server/routes/heap_profile.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { Logger, IRouter } from '@kbn/core/server';
|
||||
import { startProfiling } from '../lib/heap_profile';
|
||||
import { handleRoute } from './common';
|
||||
|
||||
const routeValidation = {
|
||||
query: schema.object({
|
||||
// seconds to run the profile
|
||||
duration: schema.number({ defaultValue: 5 }),
|
||||
// Average sample interval in bytes. The default value is 32768 bytes.
|
||||
interval: schema.number({ defaultValue: 32768 }),
|
||||
includeMajorGC: schema.boolean({ defaultValue: true }),
|
||||
includeMinorGC: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
};
|
||||
|
||||
const routeConfig = {
|
||||
path: '/_dev/heap_profile',
|
||||
validate: routeValidation,
|
||||
};
|
||||
|
||||
export function registerRoute(logger: Logger, router: IRouter): void {
|
||||
router.get(routeConfig, async (context, request, response) => {
|
||||
const { duration, interval, includeMajorGC, includeMinorGC } = request.query;
|
||||
const args = {
|
||||
samplingInterval: interval,
|
||||
includeObjectsCollectedByMajorGC: includeMajorGC,
|
||||
includeObjectsCollectedByMinorGC: includeMinorGC,
|
||||
};
|
||||
|
||||
return await handleRoute(startProfiling, args, logger, response, duration, 'heap');
|
||||
});
|
||||
}
|
17
examples/v8_profiler_examples/server/routes/index.ts
Normal file
17
examples/v8_profiler_examples/server/routes/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { Logger, IRouter } from '@kbn/core/server';
|
||||
|
||||
import { registerRoute as registerRouteCpuProfile } from './cpu_profile';
|
||||
import { registerRoute as registerRouteHeapProfile } from './heap_profile';
|
||||
|
||||
export function registerRoutes(logger: Logger, router: IRouter): void {
|
||||
registerRouteCpuProfile(logger, router);
|
||||
registerRouteHeapProfile(logger, router);
|
||||
}
|
18
examples/v8_profiler_examples/tsconfig.json
Normal file
18
examples/v8_profiler_examples/tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"server/**/*.ts",
|
||||
"../../typings/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/config-schema",
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue