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 4 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 @@ -115,6 +115,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 @@ -263,6 +264,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.29.2",
Copy link
Contributor

Choose a reason for hiding this comment

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

let's get all the 0.29s out of here and just focus on 0.30

"@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';
100 changes: 99 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,102 @@

## 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);
}
}
```

* 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be just _serializer - pretty sure the underscore is just copy-pasta from the otel js repo during my PoC work.

Copy link
Contributor Author

@crespocarlos crespocarlos Jul 12, 2022

Choose a reason for hiding this comment

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

you mean without the type? It can, but then TS will understand that this an any. Better to define the type as we're currently doing

Copy link
Contributor

Choose a reason for hiding this comment

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

without the underscore I mean :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For some reason my brain didn't read the underscore from your original comment lol. Yeah, I'll remove the underscore from these properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't hurt to leave them, but there are more examples without _ than with in the codebase.


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