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

chore: url validation & README to prevent gRPC footguns. #2130

Merged
merged 15 commits into from May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 40 additions & 16 deletions packages/opentelemetry-exporter-collector-grpc/README.md
Expand Up @@ -23,57 +23,74 @@ const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-g

const collectorOptions = {
serviceName: 'basic-service',
url: '<opentelemetry-collector-url>' // url is optional and can be omitted - default is localhost:4317
// url is optional and can be omitted - default is localhost:4317
url: '<collector-hostname>:<port>',
};

const provider = new BasicTracerProvider();
const exporter = new CollectorTraceExporter(collectorOptions);
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

provider.register();

['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => provider.shutdown().catch(console.error));
});
```

By default, plaintext connection is used. In order to use TLS in Node.js, provide `credentials` option like so:

```js
const fs = require('fs');
const grpc = require('grpc');
const grpc = require('@grpc/grpc-js');

const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing');
const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc');

const collectorOptions = {
serviceName: 'basic-service',
url: '<opentelemetry-collector-url>', // url is optional and can be omitted - default is localhost:4317
credentials: grpc.credentials.createSsl(
fs.readFileSync('./ca.crt'),
fs.readFileSync('./client.key'),
fs.readFileSync('./client.crt')
)
// url is optional and can be omitted - default is localhost:4317
url: '<collector-hostname>:<port>',
credentials: grpc.credentials.createSsl(),
};

const provider = new BasicTracerProvider();
const exporter = new CollectorTraceExporter(collectorOptions);
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

provider.register();
['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => provider.shutdown().catch(console.error));
});
```

To use mutual authentication, pass to the `createSsl()` constructor:

```js
credentials: grpc.credentials.createSsl(
fs.readFileSync('./ca.crt'),
fs.readFileSync('./client.key'),
fs.readFileSync('./client.crt')
),
```

To see how to generate credentials, you can refer to the script used to generate certificates for tests [here](./test/certs/regenerate.sh)
To generate credentials for mutual authentication, you can refer to the script used to generate certificates for tests [here](./test/certs/regenerate.sh)

The exporter can be configured to send custom metadata with each request as in the example below:

```js
const grpc = require('grpc');
const grpc = require('@grpc/grpc-js');

const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing');
const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc');

const metadata = new grpc.Metadata();
// For instance, an API key or access token might go here.
metadata.set('k', 'v');

const collectorOptions = {
serviceName: 'basic-service',
url: '<opentelemetry-collector-url>', // url is optional and can be omitted - default is localhost:4317
// url is optional and can be omitted - default is localhost:4317
url: '<collector-hostname>:<port>',
metadata, // // an optional grpc.Metadata object to be sent with each request
};

Expand All @@ -82,6 +99,9 @@ const exporter = new CollectorTraceExporter(collectorOptions);
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

provider.register();
['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => provider.shutdown().catch(console.error));
});
```

Note, that this will only work if TLS is also configured on the server.
Expand All @@ -95,20 +115,24 @@ const { MeterProvider } = require('@opentelemetry/metrics');
const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-grpc');
const collectorOptions = {
serviceName: 'basic-service',
url: '<opentelemetry-collector-url>', // url is optional and can be omitted - default is localhost:55681
// url is optional and can be omitted - default is localhost:4317
url: '<collector-hostname>:<port>',
};
const exporter = new CollectorMetricExporter(collectorOptions);

// Register the exporter
const meter = new MeterProvider({
const provider = new MeterProvider({
exporter,
interval: 60000,
}).getMeter('example-meter');
})
['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => provider.shutdown().catch(console.error));
});

// Now, start recording data
const meter = provider.getMeter('example-meter');
const counter = meter.createCounter('metric_name');
counter.add(10, { 'key': 'value' });

```

## Running opentelemetry-collector locally to see the traces
Expand Down
Expand Up @@ -26,6 +26,7 @@ import {
ServiceClientType,
} from './types';
import { ServiceClient } from './types';
import { validateAndNormalizeUrl } from './util';

/**
* Collector Metric Exporter abstract base class
Expand All @@ -41,13 +42,16 @@ export abstract class CollectorExporterNodeBase<
grpcQueue: GRPCQueueItem<ExportItem>[] = [];
metadata?: Metadata;
serviceClient?: ServiceClient = undefined;
serverAddress: string;
private _send!: Function;

constructor(config: CollectorExporterConfigNode = {}) {
super(config);
if (config.headers) {
diag.warn('Headers cannot be set when using grpc');
}

this.serverAddress = validateAndNormalizeUrl(this.url);
this.metadata = config.metadata;
}
private _sendPromise(
Expand Down
21 changes: 16 additions & 5 deletions packages/opentelemetry-exporter-collector-grpc/src/util.ts
Expand Up @@ -21,6 +21,7 @@ import { globalErrorHandler } from '@opentelemetry/core';
import { collectorTypes } from '@opentelemetry/exporter-collector';
import * as path from 'path';
import { CollectorExporterNodeBase } from './CollectorExporterNodeBase';
import { URL } from 'url';
import {
CollectorExporterConfigNode,
GRPCQueueItem,
Expand All @@ -32,7 +33,6 @@ export function onInit<ExportItem, ServiceRequest>(
config: CollectorExporterConfigNode
): void {
collector.grpcQueue = [];
const serverAddress = removeProtocol(collector.url);
const credentials: grpc.ChannelCredentials =
config.credentials || grpc.credentials.createInsecure();

Expand All @@ -52,12 +52,12 @@ export function onInit<ExportItem, ServiceRequest>(

if (collector.getServiceClientType() === ServiceClientType.SPANS) {
collector.serviceClient = new packageObject.opentelemetry.proto.collector.trace.v1.TraceService(
serverAddress,
collector.serverAddress,
credentials
);
} else {
collector.serviceClient = new packageObject.opentelemetry.proto.collector.metrics.v1.MetricsService(
serverAddress,
collector.serverAddress,
credentials
);
}
Expand Down Expand Up @@ -105,6 +105,17 @@ export function send<ExportItem, ServiceRequest>(
}
}

function removeProtocol(url: string): string {
return url.replace(/^https?:\/\//, '');
export function validateAndNormalizeUrl(url: string): string {
const target = new URL(url);
if (target.pathname !== '/') {
diag.warn(
'URL path should not be set when using grpc, the path part of the URL will be ignored.'
);
}
if (target.protocol !== '' && !target.protocol?.match(/(http|grpc)s?/)) {
diag.warn(
'URL protocol should be http(s):// or grpc(s)://. Using grpc://.'
);
}
return target.host;
}
Expand Up @@ -129,7 +129,7 @@ const testCollectorMetricExporter = (params: TestParams) =>
)
: undefined;
collectorExporter = new CollectorMetricExporter({
url: address,
url: 'grpcs://' + address,
credentials,
serviceName: 'basic-service',
metadata: params.metadata,
Expand Down Expand Up @@ -166,7 +166,7 @@ const testCollectorMetricExporter = (params: TestParams) =>

describe('instance', () => {
it('should warn about headers', () => {
// Need to stub/spy on the underlying logger as the "diag" instance is global
// Need to stub/spy on the underlying logger as the 'diag' instance is global
const spyLoggerWarn = sinon.stub(diag, 'warn');
collectorExporter = new CollectorMetricExporter({
serviceName: 'basic-service',
Expand All @@ -178,6 +178,18 @@ const testCollectorMetricExporter = (params: TestParams) =>
const args = spyLoggerWarn.args[0];
assert.strictEqual(args[0], 'Headers cannot be set when using grpc');
});
it('should warn about path in url', () => {
const spyLoggerWarn = sinon.stub(diag, 'warn');
collectorExporter = new CollectorMetricExporter({
serviceName: 'basic-service',
url: address + '/v1/metrics',
});
const args = spyLoggerWarn.args[0];
assert.strictEqual(
args[0],
'URL path should not be set when using grpc, the path part of the URL will be ignored.'
);
});
});

describe('export', () => {
Expand Down
Expand Up @@ -125,7 +125,7 @@ const testCollectorExporter = (params: TestParams) =>
: undefined;
collectorExporter = new CollectorTraceExporter({
serviceName: 'basic-service',
url: address,
url: 'grpcs://' + address,
credentials,
metadata: params.metadata,
});
Expand All @@ -143,7 +143,7 @@ const testCollectorExporter = (params: TestParams) =>

describe('instance', () => {
it('should warn about headers when using grpc', () => {
// Need to stub/spy on the underlying logger as the "diag" instance is global
// Need to stub/spy on the underlying logger as the 'diag' instance is global
const spyLoggerWarn = sinon.stub(diag, 'warn');
collectorExporter = new CollectorTraceExporter({
serviceName: 'basic-service',
Expand All @@ -155,6 +155,18 @@ const testCollectorExporter = (params: TestParams) =>
const args = spyLoggerWarn.args[0];
assert.strictEqual(args[0], 'Headers cannot be set when using grpc');
});
it('should warn about path in url', () => {
const spyLoggerWarn = sinon.stub(diag, 'warn');
collectorExporter = new CollectorTraceExporter({
serviceName: 'basic-service',
url: address + '/v1/trace',
});
const args = spyLoggerWarn.args[0];
assert.strictEqual(
args[0],
'URL path should not be set when using grpc, the path part of the URL will be ignored.'
);
});
});

describe('export', () => {
Expand Down