Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Stack Monitoring] Add OpenTelemetry metrics to Monitoring Collection plugin #135999

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d911a31
Add otel metrics to alerting plugin
matschaffer May 31, 2022
2638cbb
clean up otel poc
crespocarlos Jul 8, 2022
218a4ff
Bump @opentelemetry/api-metrics and @opentelemetry/exporter-metrics-o…
crespocarlos Jul 11, 2022
70d0ed0
Add integration test for prometheus endpoint; improve reademe.md
crespocarlos Jul 11, 2022
bb77bc1
Fix tsconfig.base.json missing entries
crespocarlos Jul 11, 2022
b595d89
Merge branch 'main' into 2220-otel-metrics-alerting-plugin
kibanamachine Jul 11, 2022
4ed979b
Bump @opentelemetry/sdk-metrics-base; clean up
crespocarlos Jul 12, 2022
f4340ed
Rename PrometheusExporter properties
crespocarlos Jul 12, 2022
67e5a64
Readme formatting tweaks
matschaffer Jul 13, 2022
6812f29
Fix incorrect path
matschaffer Jul 13, 2022
0c71fa9
Merge branch 'main' into 2220-otel-metrics-alerting-plugin
kibanamachine Jul 13, 2022
b716f3a
Remove grpc dependency
matschaffer Jul 13, 2022
92a0862
Add grpc back for handling auth headers
matschaffer Jul 13, 2022
0991876
Fix comment positioning
matschaffer Jul 13, 2022
76b7780
Include authenticated OTLP in readme
matschaffer Jul 13, 2022
052b9dd
Merge pull request #1 from matschaffer/2220-otel-metrics-alerting-plugin
crespocarlos Jul 13, 2022
533bf45
Extract dynamic route into a new file
crespocarlos Jul 13, 2022
9ab2f05
Enable otlp logging and compatibility with env vars
crespocarlos Jul 13, 2022
5c409c4
Merge branch 'main' of github.com:elastic/kibana into 2220-otel-metri…
crespocarlos Jul 14, 2022
34f19d5
Enable OTEL_EXPORTER_OTLP_ENDPOINT env var
crespocarlos Jul 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
/x-pack/plugins/monitoring/ @elastic/infra-monitoring-ui
/x-pack/test/functional/apps/monitoring @elastic/infra-monitoring-ui
/x-pack/test/api_integration/apis/monitoring @elastic/infra-monitoring-ui
/x-pack/test/api_integration/apis/monitoring-collection @elastic/infra-monitoring-ui

# Fleet
/fleet_packages.json @elastic/fleet
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"@emotion/css": "^11.9.0",
"@emotion/react": "^11.9.0",
"@emotion/serialize": "^1.0.3",
"@grpc/grpc-js": "^1.5.9",
"@hapi/accept": "^5.0.2",
"@hapi/boom": "^9.1.4",
"@hapi/cookie": "^11.0.2",
Expand Down Expand Up @@ -273,6 +274,12 @@
"@mapbox/mapbox-gl-draw": "1.3.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mapbox/vector-tile": "1.3.1",
"@opentelemetry/api-metrics": "0.30.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.30.0",
"@opentelemetry/exporter-prometheus": "^0.30.0",
"@opentelemetry/resources": "^1.3.1",
"@opentelemetry/sdk-metrics-base": "^0.30.0",
"@opentelemetry/semantic-conventions": "^1.3.1",
"@reduxjs/toolkit": "^1.6.1",
"@slack/webhook": "^5.0.4",
"@turf/along": "6.0.1",
Expand Down
2 changes: 2 additions & 0 deletions test/common/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export default function () {
`--server.maxPayload=1679958`,
// newsfeed mock service
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`,
// otel mock service
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'otel_metrics')}`,
`--newsfeed.service.urlRoot=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`,
`--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`,
// code coverage reporting plugin
Expand Down
15 changes: 15 additions & 0 deletions test/common/fixtures/plugins/otel_metrics/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "openTelemetryInstrumentedPlugin",
"owner": {
"name": "Stack Monitoring",
"githubTeam": "stack-monitoring-ui"
},
"version": "1.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": [
matschaffer marked this conversation as resolved.
Show resolved Hide resolved
"monitoringCollection"
],
"optionalPlugins": [],
"server": true,
"ui": false
}
11 changes: 11 additions & 0 deletions test/common/fixtures/plugins/otel_metrics/server/index.ts
Original file line number Diff line number Diff line change
@@ -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 { OpenTelemetryUsageTest } from './plugin';

export const plugin = () => new OpenTelemetryUsageTest();
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 { Counter, Meter } from '@opentelemetry/api-metrics';

export class Metrics {
ruleExecutions: Counter;

constructor(meter: Meter) {
this.ruleExecutions = meter.createCounter('request_count', {
description: 'Counts total number of requests',
});
}
}
28 changes: 28 additions & 0 deletions test/common/fixtures/plugins/otel_metrics/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { CoreSetup, Plugin } from '@kbn/core/server';
import { metrics } from '@opentelemetry/api-metrics';
import { generateOtelMetrics } from './routes';
import { Metrics } from './monitoring/metrics';

export class OpenTelemetryUsageTest implements Plugin {
private metrics: Metrics;

constructor() {
this.metrics = new Metrics(metrics.getMeter('dummyMetric'));
}

public setup(core: CoreSetup) {
const router = core.http.createRouter();
generateOtelMetrics(router, this.metrics);
}

public start() {}
public stop() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { IKibanaResponse, IRouter } from '@kbn/core/server';
import { Metrics } from '../monitoring/metrics';

export const generateOtelMetrics = (router: IRouter, metrics: Metrics) => {
router.post(
{
path: '/api/generate_otel_metrics',
validate: {},
},
async function (_context, _req, res): Promise<IKibanaResponse<{}>> {
metrics.ruleExecutions.add(1);
return res.ok({});
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 * from './generate_otel_metrics';
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@
"@kbn/coverage-fixtures-plugin/*": ["test/common/fixtures/plugins/coverage/*"],
"@kbn/newsfeed-fixtures-plugin": ["test/common/fixtures/plugins/newsfeed"],
"@kbn/newsfeed-fixtures-plugin/*": ["test/common/fixtures/plugins/newsfeed/*"],
"@kbn/open-telemetry-instrumented-plugin": ["test/common/fixtures/plugins/otel_metrics"],
"@kbn/open-telemetry-instrumented-plugin/*": ["test/common/fixtures/plugins/otel_metrics/*"],
"@kbn/kbn-tp-run-pipeline-plugin": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline"],
"@kbn/kbn-tp-run-pipeline-plugin/*": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline/*"],
"@kbn/app-link-test-plugin": ["test/plugin_functional/plugins/app_link_test"],
Expand Down
109 changes: 108 additions & 1 deletion x-pack/plugins/monitoring_collection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,111 @@

## Plugin

This plugin allows for other plugins to add data to Kibana stack monitoring documents.
This plugin allows for other plugins to add data to Kibana stack monitoring documents.

## OpenTelemetry Metrics

### Enable Prometheus endpoint with Elastic Agent Prometheus input

1. Start [local setup with fleet](../fleet/README.md#running-fleet-server-locally-in-a-container) or a cloud cluster
2. Start Kibana
3. Set up a new agent policy and enroll a new agent in your local machine
4. Install the Prometheus Metrics package
a. Set **Hosts** with `localhost:5601`
b. Set **Metrics Path** with `/(BASEPATH)/api/monitoring_collection/v1/prometheus`
c. Remove the values from **Bearer Token File** and **SSL Certificate Authorities**
d. Set **Username** and **Password** with `elastic` and `changeme`
5. Add the following configuration to `kibana.dev.yml`

```yml
# Enable the prometheus exporter
monitoring_collection.opentelemetry.metrics:
prometheus.enabled: true

```

### Enable OpenTelemetry Metrics API exported as OpenTelemetry Protocol

1. Start [local setup with fleet](../fleet/README.md#running-fleet-server-locally-in-a-container) or a cloud cluster
2. Start Kibana
3. Set up a new agent policy and enroll a new agent in your local machine
4. Install Elastic APM package listening on `localhost:8200` without authentication
5. Add the following configuration to `kibana.dev.yml`

```yml
# Enable the OTLP exporter
monitoring_collection.opentelemetry.metrics:
otlp.url: "http://127.0.0.1:8200"
```

### Example of how to instrument the code

* First, we need to define what metrics we want to instrument with OpenTelemetry

```ts
import { Counter, Meter } from '@opentelemetry/api-metrics';

export class FooApiMeters {
requestCount: Counter;

constructor(meter: Meter) {
this.requestCount = meter.createCounter('request_count', {
description: 'Counts total number of requests',
});
}
```

In this example we're using a `Counter` metric, but [OpenTelemetry SDK](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api_metrics.Meter.html) provides there are other options to record metrics
matschaffer marked this conversation as resolved.
Show resolved Hide resolved

* Initialize meter in the plugin setup and pass it to the relevant components that will be instrumented. In this case, we want to intrument `FooApi` routes

```ts
import { IRouter } from '@kbn/core/server';
import { FooApiMeters } from './foo_api_meters';
import { metrics } from '@opentelemetry/api-metrics';

export class FooApiPlugin implements Plugin {
private metrics: Metrics;
private libs: { router: IRouter, metrics: FooApiMeters};

constructor() {
this.metrics = new Metrics(metrics.getMeter('kibana.fooApi'));
}

public setup(core: CoreSetup) {
const router = core.http.createRouter();

this.libs = {
router,
metrics: this.metrics
}

initMetricsAPIRoute(this.libs);
}
}
```

`monitoring_collection` plugins has to be initialized before the plugin that will be instrumented. If for some reason the instrumentation doesn't record any metrics, make sure `monitoring_collection` is included in the list of `requiredPlugins`. e.g:

```json
"requiredPlugins": [
"monitoringCollection"
],
```

* Lastly we can use the `metrics` object to instrument the code

```ts
export const initMetricsAPIRoute = (libs: { router: IRouter, metrics: FooApiMeters}) => {
router.get(
{
path: '/api/foo',
validate: {},
},
async function (_context, _req, res) {
metrics.requestCount.add(1);
return res.ok({});
}
);
```

12 changes: 12 additions & 0 deletions x-pack/plugins/monitoring_collection/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import { schema, TypeOf } from '@kbn/config-schema';

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
opentelemetry: schema.object({
metrics: schema.object({
otlp: schema.object({
url: schema.maybe(schema.string()),
headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
exportIntervalMillis: schema.number({ defaultValue: 10000 }),
}),
prometheus: schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
}),
}),
});

export type MonitoringCollectionConfig = ReturnType<typeof createConfig>;
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/monitoring_collection/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
* 2.0.
*/
export const TYPE_ALLOWLIST = ['node_rules', 'cluster_rules', 'node_actions', 'cluster_actions'];

export const MONITORING_COLLECTION_BASE_PATH = '/api/monitoring_collection';
1 change: 1 addition & 0 deletions x-pack/plugins/monitoring_collection/server/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export { getKibanaStats } from './get_kibana_stats';
export { getESClusterUuid } from './get_es_cluster_uuid';
export { PrometheusExporter } from './prometheus_exporter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { AggregationTemporality, MetricReader } from '@opentelemetry/sdk-metrics-base';
import {
PrometheusExporter as OpenTelemetryPrometheusExporter,
ExporterConfig,
PrometheusSerializer,
} from '@opentelemetry/exporter-prometheus';
import { KibanaResponseFactory } from '@kbn/core/server';

export class PrometheusExporter extends MetricReader {
private readonly prefix?: string;
private readonly appendTimestamp: boolean;
private serializer: PrometheusSerializer;

constructor(config: ExporterConfig = {}) {
super();
this.prefix = config.prefix || OpenTelemetryPrometheusExporter.DEFAULT_OPTIONS.prefix;
this.appendTimestamp =
typeof config.appendTimestamp === 'boolean'
? config.appendTimestamp
: OpenTelemetryPrometheusExporter.DEFAULT_OPTIONS.appendTimestamp;

this.serializer = new PrometheusSerializer(this.prefix, this.appendTimestamp);
}

selectAggregationTemporality(): AggregationTemporality {
return AggregationTemporality.CUMULATIVE;
}

protected onForceFlush(): Promise<void> {
return Promise.resolve(undefined);
}

protected onShutdown(): Promise<void> {
return Promise.resolve(undefined);
}

/**
* Responds to incoming message with current state of all metrics.
*/
public async exportMetrics(res: KibanaResponseFactory) {
try {
const collectionResult = await this.collect();
const { resourceMetrics, errors } = collectionResult;
if (errors.length) {
return res.customError({
statusCode: 500,
body: `PrometheusExporter: Metrics collection errors ${errors}`,
});
}
const result = this.serializer.serialize(resourceMetrics);
if (result === '') {
return res.noContent();
}
return res.ok({
body: result,
});
} catch (error) {
return res.customError({
statusCode: 500,
body: {
message: `PrometheusExporter: Failed to export metrics ${error}`,
},
});
}
}
}
Loading