-
Notifications
You must be signed in to change notification settings - Fork 825
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
feat(plugin): implement redis plugin #503
Changes from 10 commits
94dc9b0
8202ac1
632b8c4
d6665c7
6c31a8a
73d2fbb
abddfe0
391326b
ca04189
0821b41
8654a71
9386c42
cf26841
b9e0047
bf1e1a7
d2e1f04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/*! | ||
* Copyright 2019, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export enum AttributeNames { | ||
// required by https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls | ||
COMPONENT = 'component', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should move these common attributes to either core or base package, this is being used in almost all the plugins. WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM. I'll create an issue for it. |
||
DB_TYPE = 'db.type', | ||
DB_INSTANCE = 'db.instance', | ||
DB_STATEMENT = 'db.statement', | ||
PEER_ADDRESS = 'peer.address', | ||
PEER_HOSTNAME = 'peer.host', | ||
|
||
// optional | ||
DB_USER = 'db.user', | ||
PEER_PORT = 'peer.port', | ||
PEER_IPV4 = 'peer.ipv4', | ||
PEER_IPV6 = 'peer.ipv6', | ||
PEER_SERVICE = 'peer.service', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
/*! | ||
* Copyright 2019, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { BasePlugin } from '@opentelemetry/core'; | ||
import * as redisTypes from 'redis'; | ||
import * as shimmer from 'shimmer'; | ||
import { SpanKind, CanonicalCode } from '@opentelemetry/types'; | ||
import { AttributeNames } from './enums'; | ||
import { | ||
RedisPluginStreamTypes, | ||
RedisPluginClientTypes, | ||
RedisCommand, | ||
} from './types'; | ||
import { EventEmitter } from 'events'; | ||
|
||
export class RedisPlugin extends BasePlugin<typeof redisTypes> { | ||
static readonly COMPONENT = 'redis'; | ||
readonly supportedVersions = ['^2.6.0']; // should be >= 2.6.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please include supported version in README.md? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 391326b |
||
|
||
constructor(readonly moduleName: string) { | ||
super(); | ||
} | ||
|
||
protected patch() { | ||
if (this._moduleExports.RedisClient) { | ||
this._logger.debug( | ||
'Patching redis.RedisClient.prototype.internal_send_command' | ||
); | ||
shimmer.wrap( | ||
this._moduleExports.RedisClient.prototype, | ||
'internal_send_command', | ||
this._getPatchInternalSendCommand() | ||
); | ||
|
||
this._logger.debug('patching redis.create_stream'); | ||
shimmer.wrap( | ||
this._moduleExports.RedisClient.prototype, | ||
'create_stream', | ||
this._getPatchCreateStream() | ||
); | ||
|
||
this._logger.debug('patching redis.createClient'); | ||
shimmer.wrap( | ||
this._moduleExports, | ||
'createClient', | ||
this._getPatchCreateClient() | ||
); | ||
} | ||
return this._moduleExports; | ||
} | ||
|
||
protected unpatch(): void { | ||
if (this._moduleExports) { | ||
shimmer.unwrap( | ||
this._moduleExports.RedisClient.prototype, | ||
'internal_send_command' | ||
); | ||
shimmer.unwrap( | ||
this._moduleExports.RedisClient.prototype, | ||
'create_stream' | ||
); | ||
shimmer.unwrap(this._moduleExports, 'createClient'); | ||
} | ||
} | ||
|
||
/** | ||
* Patch internal_send_command(...) to trace requests | ||
*/ | ||
private _getPatchInternalSendCommand() { | ||
const plugin = this; | ||
return function internal_send_command(original: Function) { | ||
return function internal_send_command_trace( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to move this function out of the class and pass in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this what you had in mind? 8654a71 |
||
this: redisTypes.RedisClient & RedisPluginClientTypes, | ||
cmd?: RedisCommand | ||
) { | ||
const parentSpan = plugin._tracer.getCurrentSpan(); | ||
|
||
// New versions of redis (2.4+) use a single options object | ||
// instead of named arguments | ||
if (arguments.length === 1 && typeof cmd === 'object') { | ||
const span = plugin._tracer.startSpan( | ||
`${RedisPlugin.COMPONENT}-${cmd.command}`, | ||
{ | ||
kind: SpanKind.CLIENT, | ||
parent: parentSpan || undefined, | ||
attributes: { | ||
[AttributeNames.COMPONENT]: RedisPlugin.COMPONENT, | ||
[AttributeNames.DB_STATEMENT]: cmd.command, | ||
}, | ||
} | ||
); | ||
|
||
// Set attributes for not explicitly typed RedisPluginClientTypes | ||
if (this.options) { | ||
span.setAttributes({ | ||
[AttributeNames.PEER_HOSTNAME]: this.options.host, | ||
[AttributeNames.PEER_PORT]: this.options.port, | ||
}); | ||
} | ||
if (this.address) { | ||
span.setAttribute( | ||
AttributeNames.PEER_ADDRESS, | ||
`redis://${this.address}` | ||
); | ||
} | ||
|
||
let spanEnded = false; | ||
const endSpan = (err?: Error | null) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be extracted as a helper function at the top level of the module? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I moved it out. I had to get rid of the |
||
if (spanEnded) return; // never | ||
if (err) { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message, | ||
}); | ||
} else { | ||
span.setStatus({ code: CanonicalCode.OK }); | ||
} | ||
span.end(); | ||
spanEnded = true; | ||
}; | ||
|
||
const originalCallback = arguments[0].callback; | ||
if (originalCallback) { | ||
(arguments[0] as RedisCommand).callback = function callback<T>( | ||
this: unknown, | ||
err: Error | null, | ||
_reply: T | ||
) { | ||
endSpan(err); | ||
return originalCallback.apply(this, arguments); | ||
}; | ||
} | ||
try { | ||
// Span will be ended in callback | ||
return original.apply(this, arguments); | ||
} catch (rethrow) { | ||
endSpan(rethrow); | ||
throw rethrow; // rethrow after ending span | ||
} | ||
} | ||
|
||
// We don't know how to trace this call, so don't start/stop a span | ||
return original.apply(this, arguments); | ||
}; | ||
}; | ||
} | ||
|
||
private _getPatchCreateClient() { | ||
const plugin = this; | ||
return function createClient(original: Function) { | ||
return function createClientTrace(this: redisTypes.RedisClient) { | ||
const client: redisTypes.RedisClient = original.apply(this, arguments); | ||
return plugin._tracer.bind(client); | ||
}; | ||
}; | ||
} | ||
|
||
private _getPatchCreateStream() { | ||
const plugin = this; | ||
return function createReadStream(original: Function) { | ||
return function create_stream_trace(this: RedisPluginStreamTypes) { | ||
if (!this.stream) { | ||
Object.defineProperty(this, 'stream', { | ||
get() { | ||
return this._patched_redis_stream; | ||
}, | ||
set(val: EventEmitter) { | ||
plugin._tracer.bind(val); | ||
this._patched_redis_stream = val; | ||
}, | ||
}); | ||
} | ||
return original.apply(this, arguments); | ||
}; | ||
}; | ||
} | ||
} | ||
|
||
export const plugin = new RedisPlugin(RedisPlugin.COMPONENT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should remove
"private": true
to make this package available for the next release?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I removed it from this plugin. I think we should also remove it from other "ready" plugins (separate PR). Seems to just be postgres and http2 currently