fix: AxiosPlugin async() / resync() (#21)

Co-authored-by: kezhenxu94 <kezhenxu94@apache.org>
diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts
index 2eaa156..8c9031b 100644
--- a/src/config/AgentConfig.ts
+++ b/src/config/AgentConfig.ts
@@ -38,9 +38,9 @@
   const ignorePath = '^(?:' + config.traceIgnorePath!.split(',').map(
     (s1) => s1.trim().split('**').map(
       (s2) => s2.split('*').map(
-        (s3) => s3.split('?').map(escapeRegExp).join('[^/]')  // replaces "**"
+        (s3) => s3.split('?').map(escapeRegExp).join('[^/]')  // replaces "?"
       ).join('[^/]*')                                         // replaces "*"
-    ).join('(?:(?:[^/]+\.)*[^/]+)?')                          // replaces "?"
+    ).join('(?:(?:[^/]+\.)*[^/]+)?')                          // replaces "**"
   ).join('|') + ')$';                                         // replaces ","
 
   config.reIgnoreOperation = RegExp(`${ignoreSuffix}|${ignorePath}`);
diff --git a/src/plugins/AxiosPlugin.ts b/src/plugins/AxiosPlugin.ts
index 7a1ac28..9612abc 100644
--- a/src/plugins/AxiosPlugin.ts
+++ b/src/plugins/AxiosPlugin.ts
@@ -21,7 +21,6 @@
 import { URL } from 'url';
 import ContextManager from '../trace/context/ContextManager';
 import { Component } from '../trace/Component';
-import Span from '../trace/span/Span';
 import Tag from '../Tag';
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
 import { createLogger } from '../logging';
@@ -37,62 +36,15 @@
     if (logger.isDebugEnabled()) {
       logger.debug('installing axios plugin');
     }
-    const axios = installer.require('axios').default;
-    this.interceptClientRequest(axios);
+
+    this.interceptClientRequest(installer);
   }
 
-  private interceptClientRequest(axios: any) {
-    const copyStatusAndStop = (span: Span, response: any) => {
-      if (response) {
-        if (response.status) {
-          span.tag(Tag.httpStatusCode(response.status));
-        }
-        if (response.statusText) {
-          span.tag(Tag.httpStatusMsg(response.statusText));
-        }
-      }
+  private interceptClientRequest(installer: PluginInstaller): void {
+    const defaults = installer.require('axios/lib/defaults');
+    const defaultAdapter = defaults.adapter;  // this will be http adapter
 
-      span.stop();
-    };
-
-    axios.interceptors.request.use(
-      (config: any) => {
-        // config.span.resync(); // TODO: fix this https://github.com/apache/skywalking-nodejs/pull/20#issuecomment-753323425
-
-        (config.span as Span).inject().items.forEach((item) => {
-          config.headers.common[item.key] = item.value;
-        });
-
-        return config;
-      },
-
-      (error: any) => {
-        error.config.span.error(error);
-        error.config.span.stop();
-
-        return Promise.reject(error);
-      },
-    );
-
-    axios.interceptors.response.use(
-      (response: any) => {
-        copyStatusAndStop(response.config.span, response);
-
-        return response;
-      },
-
-      (error: any) => {
-        error.config.span.error(error);
-
-        copyStatusAndStop(error.config.span, error.response);
-
-        return Promise.reject(error);
-      },
-    );
-
-    const _request = axios.Axios.prototype.request;
-
-    axios.Axios.prototype.request = function(config: any) {
+    defaults.adapter = (config: any) => {
       const { host, pathname: operation } = new URL(config.url);  // TODO: this may throw invalid URL
       const span = ContextManager.current.newExitSpan(operation, host).start();
 
@@ -101,16 +53,50 @@
         span.layer = SpanLayer.HTTP;
         span.peer = host;
         span.tag(Tag.httpURL(host + operation));
-        // span.async(); TODO: fix this https://github.com/apache/skywalking-nodejs/pull/20#issuecomment-753323425
 
-        return _request.call(this, { ...config, span });
+        span.inject().items.forEach((item) => {
+          config.headers[item.key] = item.value;
+        });
+
+        const copyStatusAndStop = (response: any) => {
+          if (response) {
+            if (response.status) {
+              span.tag(Tag.httpStatusCode(response.status));
+              if (response.status >= 400) {
+                span.errored = true;
+              }
+            }
+
+            if (response.statusText) {
+              span.tag(Tag.httpStatusMsg(response.statusText));
+            }
+          }
+
+          span.stop();
+        };
+
+        return defaultAdapter(config).then(
+          (response: any) => {
+            copyStatusAndStop(response);
+
+            return response;
+          },
+
+          (error: any) => {
+            span.error(error);
+            copyStatusAndStop(error.response);
+
+            return Promise.reject(error);
+          }
+        );
+
       } catch (e) {
         span.error(e);
         span.stop();
 
         throw e;
       }
-    };
+    }
   }
 }
 
diff --git a/src/plugins/HttpPlugin.ts b/src/plugins/HttpPlugin.ts
index e679ec9..ca01811 100644
--- a/src/plugins/HttpPlugin.ts
+++ b/src/plugins/HttpPlugin.ts
@@ -27,6 +27,8 @@
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from '../trace/context/ContextCarrier';
 
+const NativePromise = (async () => null)().constructor;  // may be different from globally overridden Promise
+
 class HttpPlugin implements SwPlugin {
   readonly module = 'http';
   readonly versions = '*';
@@ -75,30 +77,33 @@
           span.tag(Tag.httpURL(httpURL));
         }
 
-        const request: ClientRequest = _request.apply(this, arguments);
+        const req: ClientRequest = _request.apply(this, arguments);
 
         span.inject().items.forEach((item) => {
-          request.setHeader(item.key, item.value);
+          req.setHeader(item.key, item.value);
         });
 
-        request.on('close', stopIfNotStopped);
-        request.on('abort', () => (span.errored = true, stopIfNotStopped()));
-        request.on('error', (err) => (span.error(err), stopIfNotStopped()));
+        req.on('close', stopIfNotStopped);
+        req.on('abort', () => (span.errored = true, stopIfNotStopped()));
+        req.on('error', (err) => (span.error(err), stopIfNotStopped()));
 
-        request.prependListener('response', (res) => {
+        req.prependListener('response', (res) => {
           span.resync();
           span.tag(Tag.httpStatusCode(res.statusCode));
+
           if (res.statusCode && res.statusCode >= 400) {
             span.errored = true;
           }
           if (res.statusMessage) {
             span.tag(Tag.httpStatusMsg(res.statusMessage));
           }
+
+          res.on('end', stopIfNotStopped);
         });
 
         span.async();
 
-        return request;
+        return req;
 
       } catch (e) {
         if (!stopped) {  // don't want to set error if exception occurs after clean close
@@ -112,45 +117,74 @@
   }
 
   private interceptServerRequest(module: any) {
-    const _emit = module.Server.prototype.emit;
+    /// TODO? full event protocol support not currently implemented (prependListener(), removeListener(), etc...)
+    const _addListener = module.Server.prototype.addListener;
 
-    module.Server.prototype.emit = function () {
-      if (arguments[0] !== 'request') {
-        return _emit.apply(this, arguments);
-      }
+    module.Server.prototype.addListener = module.Server.prototype.on = function (event: any, handler: any, ...addArgs: any[]) {
+      return _addListener.call(this, event, event === 'request' ? _sw_request : handler, ...addArgs);
 
-      const [req, res] = [arguments[1] as IncomingMessage, arguments[2] as ServerResponse];
+      function _sw_request(this: any, req: IncomingMessage, res: ServerResponse, ...reqArgs: any[]) {
+        const headers = req.rawHeaders || [];
+        const headersMap: { [key: string]: string } = {};
 
-      const headers = req.rawHeaders || [];
-      const headersMap: { [key: string]: string } = {};
-
-      for (let i = 0; i < headers.length / 2; i += 2) {
-        headersMap[headers[i]] = headers[i + 1];
-      }
-
-      const carrier = ContextCarrier.from(headersMap);
-      const operation = (req.url || '/').replace(/\?.*/g, '');
-      const span = ContextManager.current.newEntrySpan(operation, carrier);
-
-      return ContextManager.withSpan(span, (self, args) => {
-        span.component = Component.HTTP_SERVER;
-        span.layer = SpanLayer.HTTP;
-        span.peer = req.headers.host || '';
-        span.tag(Tag.httpURL(span.peer + req.url));
-
-        const ret = _emit.apply(self, args);
-
-        span.tag(Tag.httpStatusCode(res.statusCode));
-        if (res.statusCode && res.statusCode >= 400) {
-          span.errored = true;
-        }
-        if (res.statusMessage) {
-          span.tag(Tag.httpStatusMsg(res.statusMessage));
+        for (let i = 0; i < headers.length / 2; i += 2) {
+          headersMap[headers[i]] = headers[i + 1];
         }
 
-        return ret;
+        const carrier = ContextCarrier.from(headersMap);
+        const operation = (req.url || '/').replace(/\?.*/g, '');
+        const span = ContextManager.current.newEntrySpan(operation, carrier).start();
 
-      }, this, arguments);
+        const copyStatusAndStop = () => {
+          span.tag(Tag.httpStatusCode(res.statusCode));
+          if (res.statusCode && res.statusCode >= 400) {
+            span.errored = true;
+          }
+          if (res.statusMessage) {
+            span.tag(Tag.httpStatusMsg(res.statusMessage));
+          }
+
+          span.stop();
+        };
+
+        try {
+          span.component = Component.HTTP_SERVER;
+          span.layer = SpanLayer.HTTP;
+          span.peer = req.connection.remoteFamily === 'IPv6'
+            ? `[${req.connection.remoteAddress}]:${req.connection.remotePort}`
+            : `${req.connection.remoteAddress}:${req.connection.remotePort}`;
+          span.tag(Tag.httpURL((req.headers.host || '') + req.url));
+
+          let ret = handler.call(this, req, res, ...reqArgs);
+          const type = ret?.constructor;
+
+          if (type !== Promise && type !== NativePromise) {
+            copyStatusAndStop();
+
+          } else {
+            ret = ret.then((r: any) => {
+              copyStatusAndStop();
+
+              return r;
+            },
+
+            (error: any) => {
+              span.error(error);
+              span.stop();
+
+              return Promise.reject(error);
+            })
+          }
+
+          return ret;
+
+        } catch (e) {
+          span.error(e);
+          span.stop();
+
+          throw e;
+        }
+      }
     };
   }
 }
diff --git a/tests/plugins/axios/client.ts b/tests/plugins/axios/client.ts
index 91f435d..bcc3308 100644
--- a/tests/plugins/axios/client.ts
+++ b/tests/plugins/axios/client.ts
@@ -26,8 +26,8 @@
   maxBufferSize: 1000,
 });
 
-const server = http.createServer((req, res) => {
-  axios
+const server = http.createServer(async (req, res) => {
+  await axios
   .get(`http://${process.env.SERVER || 'localhost:5000'}${req.url}`)
   .then((r) => res.end(JSON.stringify(r.data)))
   .catch(err => res.end(JSON.stringify(err)));
diff --git a/tests/plugins/axios/expected.data.yaml b/tests/plugins/axios/expected.data.yaml
index fb28cf4..3ecd711 100644
--- a/tests/plugins/axios/expected.data.yaml
+++ b/tests/plugins/axios/expected.data.yaml
@@ -21,16 +21,42 @@
     segments:
       - segmentId: not null
         spans:
+          - operationName: /json
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 4005
+            spanType: Exit
+            peer: httpbin.org
+            skipAnalysis: false
+            tags:
+              - key: http.url
+                value: httpbin.org:80/json
+              - key: http.status.code
+                value: '200'
+              - key: http.status.msg
+                value: OK
           - operationName: /axios
             operationId: 0
             parentSpanId: -1
             spanId: 0
             spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 49
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
             tags:
               - key: http.url
                 value: server:5000/axios
               - key: http.status.code
                 value: '200'
+              - key: http.status.msg
+                value: OK
             refs:
               - parentEndpoint: ''
                 networkAddress: server:5000
@@ -40,20 +66,19 @@
                 parentServiceInstance: not null
                 parentService: client
                 traceId: not null
-            startTime: gt 0
-            endTime: gt 0
-            componentId: 49
-            spanType: Entry
-            peer: server:5000
-            skipAnalysis: false
-          - operationName: /json
+  - serviceName: client
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /axios
             operationId: 0
             parentSpanId: 0
             spanId: 1
             spanLayer: Http
             tags:
               - key: http.url
-                value: httpbin.org:80/json
+                value: server:5000/axios
               - key: http.status.code
                 value: '200'
               - key: http.status.msg
@@ -62,13 +87,8 @@
             endTime: gt 0
             componentId: 4005
             spanType: Exit
-            peer: httpbin.org
+            peer: server:5000
             skipAnalysis: false
-  - serviceName: client
-    segmentSize: 1
-    segments:
-      - segmentId: not null
-        spans:
           - operationName: /axios
             operationId: 0
             parentSpanId: -1
@@ -79,27 +99,11 @@
                 value: localhost:5001/axios
               - key: http.status.code
                 value: '200'
-            startTime: gt 0
-            endTime: gt 0
-            componentId: 49
-            spanType: Entry
-            peer: localhost:5001
-            skipAnalysis: false
-          - operationName: /axios
-            operationId: 0
-            parentSpanId: 0
-            spanId: 1
-            spanLayer: Http
-            tags:
-              - key: http.url
-                value: server:5000/axios
-              - key: http.status.code
-                value: '200'
               - key: http.status.msg
                 value: OK
             startTime: gt 0
             endTime: gt 0
-            componentId: 4005
-            spanType: Exit
-            peer: server:5000
+            componentId: 49
+            spanType: Entry
+            peer: not null
             skipAnalysis: false
diff --git a/tests/plugins/express/expected.data.yaml b/tests/plugins/express/expected.data.yaml
index 9cf0fad..009c233 100644
--- a/tests/plugins/express/expected.data.yaml
+++ b/tests/plugins/express/expected.data.yaml
@@ -21,6 +21,24 @@
     segments:
       - segmentId: not null
         spans:
+          - operationName: /json
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 2
+            spanType: Exit
+            peer: httpbin.org
+            skipAnalysis: false
+            tags:
+              - key: http.url
+                value: httpbin.org/json
+              - key: http.status.code
+                value: '200'
+              - key: http.status.msg
+                value: OK
           - operationName: /express
             operationId: 0
             parentSpanId: -1
@@ -48,24 +66,6 @@
             spanType: Entry
             peer: server:5000
             skipAnalysis: false
-          - operationName: /json
-            operationId: 0
-            parentSpanId: 0
-            spanId: 1
-            spanLayer: Http
-            tags:
-              - key: http.url
-                value: httpbin.org/json
-              - key: http.status.code
-                value: '200'
-              - key: http.status.msg
-                value: OK
-            startTime: gt 0
-            endTime: gt 0
-            componentId: 2
-            spanType: Exit
-            peer: httpbin.org
-            skipAnalysis: false
   - serviceName: client
     segmentSize: 1
     segments:
@@ -73,24 +73,6 @@
         spans:
           - operationName: /express
             operationId: 0
-            parentSpanId: -1
-            spanId: 0
-            spanLayer: Http
-            tags:
-              - key: http.url
-                value: localhost:5001/express
-              - key: http.status.code
-                value: '200'
-              - key: http.status.msg
-                value: OK
-            startTime: gt 0
-            endTime: gt 0
-            componentId: 4002
-            spanType: Entry
-            peer: localhost:5001
-            skipAnalysis: false
-          - operationName: /express
-            operationId: 0
             parentSpanId: 0
             spanId: 1
             spanLayer: Http
@@ -107,3 +89,21 @@
             spanType: Exit
             peer: server:5000
             skipAnalysis: false
+          - operationName: /express
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            tags:
+              - key: http.url
+                value: localhost:5001/express
+              - key: http.status.code
+                value: '200'
+              - key: http.status.msg
+                value: OK
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 4002
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
diff --git a/tests/plugins/http/expected.data.yaml b/tests/plugins/http/expected.data.yaml
index dfb19ac..b498ff7 100644
--- a/tests/plugins/http/expected.data.yaml
+++ b/tests/plugins/http/expected.data.yaml
@@ -30,7 +30,7 @@
             endTime: gt 0
             componentId: 49
             spanType: Entry
-            peer: server:5000
+            peer: not null
             skipAnalysis: false
             tags:
               - key: http.url
@@ -78,7 +78,7 @@
             endTime: gt 0
             componentId: 49
             spanType: Entry
-            peer: localhost:5001
+            peer: not null
             skipAnalysis: false
             tags:
               - key: http.url