diff --git a/README.md b/README.md index 0b1b9f0..94c2d0c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Environment Variable | Description | Default | `SW_AGENT_LOGGING_LEVEL` | The logging level, could be one of `CRITICAL`, `FATAL`, `ERROR`, `WARN`(`WARNING`), `INFO`, `DEBUG` | `INFO` | | `SW_IGNORE_SUFFIX` | The suffices of endpoints that will be ignored (not traced), comma separated | `.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg` | | `SW_TRACE_IGNORE_PATH` | The paths of endpoints that will be ignored (not traced), comma separated | `` | +| `SW_MYSQL_SQL_PARAMETERS_MAX_LENGTH` | The maximum string length of MySQL parameters to log | `512` | | `SW_AGENT_MAX_BUFFER_SIZE` | The maximum buffer size before sending the segment data to backend | `'1000'` | ## Supported Libraries @@ -65,9 +66,20 @@ There are some built-in plugins that support automatic instrumentation of NodeJS Library | Plugin Name | :--- | :--- | -| built-in `http` and `https` module | `http` | +| built-in `http` and `https` module | `http` / `https` | | [`express`](https://expressjs.com) | `express` | | [`axios`](https://github.com/axios/axios) | `axios` | +| [`mysql`](https://github.com/mysqljs/mysql) | `mysql` | + +### Compatible Libraries + +The following are packages that have been tested to some extent and are compatible because they work through the instrumentation of an underlying package: + +Library | Underlying Plugin Name +| :--- | :--- | +| [`request`](https://github.com/request/request) | `http` / `https` | +| [`request-promise`](https://github.com/request/request-promise) | `http` / `https` | +| [`koa`](https://github.com/koajs/koa) | `http` / `https` | ## Contact Us * Submit [an issue](https://github.com/apache/skywalking/issues/new) by using [Nodejs] as title prefix. diff --git a/src/Tag.ts b/src/Tag.ts index 95e8ab3..305107c 100644 --- a/src/Tag.ts +++ b/src/Tag.ts @@ -24,19 +24,25 @@ export interface Tag { } export default { + httpStatusCodeKey: 'http.status.code', // TODO: maybe find a better place to put these? + httpStatusMsgKey: 'http.status.msg', httpURLKey: 'http.url', - httpMethodKey: 'http.method', // TODO: maybe find a better place to put these? + httpMethodKey: 'http.method', + dbTypeKey: 'db.type', + dbInstanceKey: 'db.instance', + dbStatementKey: 'db.statement', + dbSqlParametersKey: 'db.sql.parameters', httpStatusCode(val: string | number | undefined): Tag { return { - key: 'http.status.code', + key: this.httpStatusCodeKey, overridable: true, val: `${val}`, } as Tag; }, httpStatusMsg(val: string | undefined): Tag { return { - key: 'http.status.msg', + key: this.httpStatusMsgKey, overridable: true, val: `${val}`, } as Tag; @@ -55,4 +61,32 @@ export default { val: `${val}`, } as Tag; }, + dbType(val: string | undefined): Tag { + return { + key: this.dbTypeKey, + overridable: true, + val: `${val}`, + } as Tag; + }, + dbInstance(val: string | undefined): Tag { + return { + key: this.dbInstanceKey, + overridable: true, + val: `${val}`, + } as Tag; + }, + dbStatement(val: string | undefined): Tag { + return { + key: this.dbStatementKey, + overridable: true, + val: `${val}`, + } as Tag; + }, + dbSqlParameters(val: string | undefined): Tag { + return { + key: this.dbSqlParametersKey, + overridable: false, + val: `${val}`, + } as Tag; + }, }; diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts index 8c9031b..de68ccb 100644 --- a/src/config/AgentConfig.ts +++ b/src/config/AgentConfig.ts @@ -27,6 +27,7 @@ export type AgentConfig = { maxBufferSize?: number; ignoreSuffix?: string; traceIgnorePath?: string; + mysql_sql_parameters_max_length?: number; // the following is internal state computed from config values reIgnoreOperation?: RegExp; }; @@ -59,5 +60,6 @@ export default { Number.parseInt(process.env.SW_AGENT_MAX_BUFFER_SIZE as string, 10) : 1000, ignoreSuffix: process.env.SW_IGNORE_SUFFIX ?? '.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg', traceIgnorePath: process.env.SW_TRACE_IGNORE_PATH || '', + mysql_sql_parameters_max_length: Math.trunc(Math.max(0, Number(process.env.SW_MYSQL_SQL_PARAMETERS_MAX_LENGTH))) || 512, reIgnoreOperation: RegExp(''), // temporary placeholder so Typescript doesn't throw a fit }; diff --git a/src/plugins/MySQLPlugin.ts b/src/plugins/MySQLPlugin.ts new file mode 100644 index 0000000..6a3e6dd --- /dev/null +++ b/src/plugins/MySQLPlugin.ts @@ -0,0 +1,143 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 SwPlugin from '../core/SwPlugin'; +import ContextManager from '../trace/context/ContextManager'; +import { Component } from '../trace/Component'; +import Tag from '../Tag'; +import { SpanLayer } from '../proto/language-agent/Tracing_pb'; +import { createLogger } from '../logging'; +import PluginInstaller from '../core/PluginInstaller'; +import config from '../config/AgentConfig'; + +const logger = createLogger(__filename); + +class MySQLPlugin implements SwPlugin { + readonly module = 'mysql'; + readonly versions = '*'; + + install(installer: PluginInstaller): void { + if (logger.isDebugEnabled()) { + logger.debug('installing mysql plugin'); + } + + const Connection = installer.require('mysql/lib/Connection'); + const _query = Connection.prototype.query; + + Connection.prototype.query = function(sql: any, values: any, cb: any) { + const wrapCallback = (_cb: any) => { + return function(this: any, error: any, results: any, fields: any) { + if (error) + span.error(error); + + span.stop(); + + return _cb.call(this, error, results, fields); + } + }; + + const host = `${this.config.host}:${this.config.port}`; + const span = ContextManager.current.newExitSpan('mysql/query', host).start(); + + try { + let _sql: any; + let _values: any; + let streaming: any; + + if (typeof sql === 'function') { + sql = wrapCallback(sql); + + } else if (typeof sql === 'object') { + _sql = sql.sql; + + if (typeof values === 'function') { + values = wrapCallback(values); + _values = sql.values; + + } else if (values !== undefined) { + _values = values; + + if (typeof cb === 'function') { + cb = wrapCallback(cb); + } else { + streaming = true; + } + + } else { + streaming = true; + } + + } else { + _sql = sql; + + if (typeof values === 'function') { + values = wrapCallback(values); + + } else if (values !== undefined) { + _values = values; + + if (typeof cb === 'function') { + cb = wrapCallback(cb); + } else { + streaming = true; + } + + } else { + streaming = true; + } + } + + span.component = Component.MYSQL; + span.layer = SpanLayer.DATABASE; + span.peer = host; + + span.tag(Tag.dbType('mysql')); + span.tag(Tag.dbInstance(this.config.database || '')); + span.tag(Tag.dbStatement(_sql || '')); + + if (_values) { + let vals = _values.map((v: any) => `${v}`).join(', '); + + if (vals.length > config.mysql_sql_parameters_max_length) + vals = vals.splice(0, config.mysql_sql_parameters_max_length); + + span.tag(Tag.dbSqlParameters(`[${vals}]`)); + } + + const query = _query.call(this, sql, values, cb); + + if (streaming) { + query.on('error', (e: any) => span.error(e)); + query.on('end', () => span.stop()); + } + + return query; + + } catch (e) { + span.error(e); + span.stop(); + + throw e; + } + }; + } +} + +// noinspection JSUnusedGlobalSymbols +export default new MySQLPlugin(); diff --git a/src/trace/Component.ts b/src/trace/Component.ts index 1eb9597..6469900 100644 --- a/src/trace/Component.ts +++ b/src/trace/Component.ts @@ -20,6 +20,7 @@ export class Component { static readonly UNKNOWN = new Component(0); static readonly HTTP = new Component(2); + static readonly MYSQL = new Component(5); static readonly MONGODB = new Component(9); static readonly HTTP_SERVER = new Component(49); static readonly EXPRESS = new Component(4002);