Added http ignore by method (#49)

diff --git a/README.md b/README.md
index 1ff9800..de6acf0 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@
 | `SW_AGENT_DISABLE_PLUGINS` | Comma-delimited list of plugins to disable in the plugins directory (e.g. "mysql", "express"). | `` |
 | `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_HTTP_IGNORE_METHOD` | Comma-delimited list of http methods to ignore (GET, POST, HEAD, OPTIONS, etc...) | `` |
 | `SW_SQL_TRACE_PARAMETERS` | If set to 'true' then SQL query parameters will be included | `false` |
 | `SW_SQL_PARAMETERS_MAX_LENGTH` | The maximum string length of SQL parameters to log | `512` |
 | `SW_MONGO_TRACE_PARAMETERS` | If set to 'true' then mongodb query parameters will be included | `false` |
diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts
index eb74b0d..59c2c04 100644
--- a/src/config/AgentConfig.ts
+++ b/src/config/AgentConfig.ts
@@ -29,6 +29,7 @@
   disablePlugins?: string;
   ignoreSuffix?: string;
   traceIgnorePath?: string;
+  httpIgnoreMethod?: string;
   sqlTraceParameters?: boolean;
   sqlParametersMaxLength?: number;
   mongoTraceParameters?: boolean;
@@ -36,6 +37,7 @@
   // the following is internal state computed from config values
   reDisablePlugins?: RegExp;
   reIgnoreOperation?: RegExp;
+  reHttpIgnoreMethod?: RegExp;
 };
 
 export function finalizeConfig(config: AgentConfig): void {
@@ -53,9 +55,10 @@
   ).join('|') + ')$';                                         // replaces ","
 
   config.reIgnoreOperation = RegExp(`${ignoreSuffix}|${ignorePath}`);
+  config.reHttpIgnoreMethod = RegExp(`^(?:${config.httpIgnoreMethod!.split(',').map((s) => s.trim()).join('|')})$`, 'i');
 }
 
-export default {
+const _config = {
   serviceName: process.env.SW_AGENT_NAME || 'your-nodejs-service',
   serviceInstance:
     process.env.SW_AGENT_INSTANCE ||
@@ -70,10 +73,18 @@
   disablePlugins: process.env.SW_AGENT_DISABLE_PLUGINS || '',
   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 || '',
+  httpIgnoreMethod: process.env.SW_HTTP_IGNORE_METHOD || '',
   sqlTraceParameters: (process.env.SW_SQL_TRACE_PARAMETERS || '').toLowerCase() === 'true',
   sqlParametersMaxLength: Math.trunc(Math.max(0, Number(process.env.SW_SQL_PARAMETERS_MAX_LENGTH))) || 512,
   mongoTraceParameters: (process.env.SW_MONGO_TRACE_PARAMETERS || '').toLowerCase() === 'true',
   mongoParametersMaxLength: Math.trunc(Math.max(0, Number(process.env.SW_MONGO_PARAMETERS_MAX_LENGTH))) || 512,
   reDisablePlugins: RegExp(''),  // temporary placeholder so Typescript doesn't throw a fit
   reIgnoreOperation: RegExp(''),
+  reHttpIgnoreMethod: RegExp(''),
 };
+
+export default _config;
+
+export function ignoreHttpMethodCheck(method: string): boolean {
+  return Boolean(method.match(_config.reHttpIgnoreMethod));
+}
diff --git a/src/index.ts b/src/index.ts
index 1dfae71..3f455b8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -33,12 +33,14 @@
       return;
     }
 
+    if (this.started) {
+      logger.warn('SkyWalking agent started more than once, subsequent options to start ignored.');
+      return;
+    }
+
     Object.assign(config, options);
     finalizeConfig(config);
 
-    if (this.started) {
-      throw new Error('SkyWalking agent is already started and can only be started once.');
-    }
     logger.debug('Starting SkyWalking agent');
 
     this.started = true;
diff --git a/src/plugins/AxiosPlugin.ts b/src/plugins/AxiosPlugin.ts
index 1684c99..1eedd67 100644
--- a/src/plugins/AxiosPlugin.ts
+++ b/src/plugins/AxiosPlugin.ts
@@ -17,12 +17,14 @@
  *
  */
 
-import SwPlugin, {wrapPromise} from '../core/SwPlugin';
+import SwPlugin from '../core/SwPlugin';
 import { URL } from 'url';
 import ContextManager from '../trace/context/ContextManager';
 import { Component } from '../trace/Component';
 import Tag from '../Tag';
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
+import DummySpan from '../trace/span/DummySpan';
+import { ignoreHttpMethodCheck } from '../config/AgentConfig';
 import PluginInstaller from '../core/PluginInstaller';
 
 class AxiosPlugin implements SwPlugin {
@@ -44,7 +46,10 @@
         config = url ? {...url} : {};
 
       const {origin, host, pathname: operation} = new URL(config.url);  // TODO: this may throw invalid URL
-      const span = ContextManager.current.newExitSpan(operation, host, Component.AXIOS, Component.HTTP);
+      const method = (config.method || 'GET').toUpperCase();
+      const span = ignoreHttpMethodCheck(method)
+        ? DummySpan.create()
+        : ContextManager.current.newExitSpan(operation, host, Component.AXIOS, Component.HTTP);
 
       span.start();
 
@@ -56,7 +61,7 @@
         span.peer = host;
 
         span.tag(Tag.httpURL(origin + operation));
-        span.tag(Tag.httpMethod((config.method || 'GET').toUpperCase()));
+        span.tag(Tag.httpMethod(method));
 
         span.inject().items.forEach((item) => config.headers[item.key] = item.value);
 
diff --git a/src/plugins/ExpressPlugin.ts b/src/plugins/ExpressPlugin.ts
index 10ae4f9..d9b25f0 100644
--- a/src/plugins/ExpressPlugin.ts
+++ b/src/plugins/ExpressPlugin.ts
@@ -22,8 +22,9 @@
 import ContextManager from '../trace/context/ContextManager';
 import { Component } from '../trace/Component';
 import Tag from '../Tag';
-import EntrySpan from '../trace/span/EntrySpan';
 import { ContextCarrier } from '../trace/context/ContextCarrier';
+import DummySpan from '../trace/span/DummySpan';
+import { ignoreHttpMethodCheck } from '../config/AgentConfig';
 import PluginInstaller from '../core/PluginInstaller';
 import HttpPlugin from './HttpPlugin';
 
@@ -42,7 +43,9 @@
     router.handle = function(req: IncomingMessage, res: ServerResponse, next: any) {
       const carrier = ContextCarrier.from((req as any).headers || {});
       const operation = (req.url || '/').replace(/\?.*/g, '');
-      const span: EntrySpan = ContextManager.current.newEntrySpan(operation, carrier, Component.HTTP_SERVER) as EntrySpan;
+      const span = ignoreHttpMethodCheck(req.method ?? 'GET')
+        ? DummySpan.create()
+        : ContextManager.current.newEntrySpan(operation, carrier, Component.HTTP_SERVER);
 
       span.component = Component.EXPRESS;
 
diff --git a/src/plugins/HttpPlugin.ts b/src/plugins/HttpPlugin.ts
index c9936e3..22c37f4 100644
--- a/src/plugins/HttpPlugin.ts
+++ b/src/plugins/HttpPlugin.ts
@@ -24,9 +24,10 @@
 import { Component } from '../trace/Component';
 import Tag from '../Tag';
 import Span from '../trace/span/Span';
-import ExitSpan from '../trace/span/ExitSpan';
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from '../trace/context/ContextCarrier';
+import DummySpan from '../trace/span/DummySpan';
+import { ignoreHttpMethodCheck } from '../config/AgentConfig';
 
 class HttpPlugin implements SwPlugin {
   readonly module = 'http';
@@ -59,7 +60,10 @@
           };
 
       const operation = pathname.replace(/\?.*$/g, '');
-      const span: ExitSpan = ContextManager.current.newExitSpan(operation, host, Component.HTTP) as ExitSpan;
+      const method = arguments[url instanceof URL || typeof url === 'string' ? 1 : 0]?.method || 'GET';
+      const span = ignoreHttpMethodCheck(method)
+        ? DummySpan.create()
+        : ContextManager.current.newExitSpan(operation, host, Component.HTTP);
 
       if (span.depth)  // if we inherited from a higher level plugin then do nothing, higher level should do all the work and we don't duplicate here
         return _request.apply(this, arguments);
@@ -72,7 +76,7 @@
         span.peer = host;
 
         span.tag(Tag.httpURL(protocol + '://' + host + pathname));
-        span.tag(Tag.httpMethod(arguments[url instanceof URL || typeof url === 'string' ? 1 : 0]?.method || 'GET'));
+        span.tag(Tag.httpMethod(method));
 
         const copyStatusAndWrapEmit = (res: any) => {
           span.tag(Tag.httpStatusCode(res.statusCode));
@@ -146,7 +150,9 @@
       function _sw_request(this: any, req: IncomingMessage, res: ServerResponse, ...reqArgs: any[]) {
         const carrier = ContextCarrier.from((req as any).headers || {});
         const operation = (req.url || '/').replace(/\?.*/g, '');
-        const span = ContextManager.current.newEntrySpan(operation, carrier);
+        const span = ignoreHttpMethodCheck(req.method ?? 'GET')
+          ? DummySpan.create()
+          : ContextManager.current.newEntrySpan(operation, carrier);
 
         span.component = Component.HTTP_SERVER;
 
@@ -168,7 +174,7 @@
           ? `[${req.connection.remoteAddress}]:${req.connection.remotePort}`
           : `${req.connection.remoteAddress}:${req.connection.remotePort}`);
 
-      span.tag(Tag.httpMethod(req.method));
+      span.tag(Tag.httpMethod(req.method ?? 'GET'));
 
       const ret = handler();
 
diff --git a/src/trace/context/DummyContext.ts b/src/trace/context/DummyContext.ts
index f08c93d..2d5c4fb 100644
--- a/src/trace/context/DummyContext.ts
+++ b/src/trace/context/DummyContext.ts
@@ -22,15 +22,10 @@
 import DummySpan from '../../trace/span/DummySpan';
 import Segment from '../../trace/context/Segment';
 import { Component } from '../../trace/Component';
-import { SpanType } from '../../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from './ContextCarrier';
 
 export default class DummyContext implements Context {
-  span: Span = new DummySpan({
-    context: this,
-    operation: '',
-    type: SpanType.LOCAL,
-  });
+  span: Span = DummySpan.create(this);
   segment: Segment = new Segment();
   nSpans = 0;
 
diff --git a/src/trace/context/SpanContext.ts b/src/trace/context/SpanContext.ts
index 29241cd..2436bd0 100644
--- a/src/trace/context/SpanContext.ts
+++ b/src/trace/context/SpanContext.ts
@@ -19,7 +19,6 @@
 
 import config from '../../config/AgentConfig';
 import Context from '../../trace/context/Context';
-import DummyContext from '../../trace/context/DummyContext';
 import Span from '../../trace/span/Span';
 import DummySpan from '../../trace/span/DummySpan';
 import Segment from '../../trace/context/Segment';
@@ -53,13 +52,8 @@
   }
 
   ignoreCheck(operation: string, type: SpanType): Span | undefined {
-    if (operation.match(config.reIgnoreOperation)) {
-      return new DummySpan({
-        context: new DummyContext(),
-        operation: '',
-        type,
-      });
-    }
+    if (operation.match(config.reIgnoreOperation))
+      return DummySpan.create();
 
     return undefined;
   }
diff --git a/src/trace/span/DummySpan.ts b/src/trace/span/DummySpan.ts
index 8657bfb..c2b1571 100644
--- a/src/trace/span/DummySpan.ts
+++ b/src/trace/span/DummySpan.ts
@@ -19,8 +19,19 @@
 
 import Span from '../../trace/span/Span';
 import { ContextCarrier } from '../context/ContextCarrier';
+import Context from '../context/Context';
+import { SpanType } from '../../proto/language-agent/Tracing_pb';
+import DummyContext from '../context/DummyContext';
 
 export default class DummySpan extends Span {
+  static create(context?: Context): DummySpan {
+    return new DummySpan({
+      context: context ?? new DummyContext(),
+      operation: '',
+      type: SpanType.LOCAL,
+    });
+  }
+
   inject(): ContextCarrier {
     return new ContextCarrier();
   }
diff --git a/src/trace/span/EntrySpan.ts b/src/trace/span/EntrySpan.ts
index 511b0a5..3532c75 100644
--- a/src/trace/span/EntrySpan.ts
+++ b/src/trace/span/EntrySpan.ts
@@ -17,13 +17,13 @@
  *
  */
 
-import StackedSpan from '../../trace/span/StackedSpan';
+import Span from '../../trace/span/Span';
 import { SpanCtorOptions } from './Span';
 import SegmentRef from '../../trace/context/SegmentRef';
 import { SpanType } from '../../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from '../context/ContextCarrier';
 
-export default class EntrySpan extends StackedSpan {
+export default class EntrySpan extends Span {
   constructor(options: SpanCtorOptions) {
     super(
       Object.assign(options, {
diff --git a/src/trace/span/ExitSpan.ts b/src/trace/span/ExitSpan.ts
index 745e4c4..a120366 100644
--- a/src/trace/span/ExitSpan.ts
+++ b/src/trace/span/ExitSpan.ts
@@ -17,14 +17,14 @@
  *
  */
 
-import StackedSpan from '../../trace/span/StackedSpan';
+import Span from '../../trace/span/Span';
 import { SpanCtorOptions } from './Span';
 import config from '../../config/AgentConfig';
 import { SpanType } from '../../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from '../context/ContextCarrier';
 import ContextManager from '../context/ContextManager';
 
-export default class ExitSpan extends StackedSpan {
+export default class ExitSpan extends Span {
   constructor(options: SpanCtorOptions) {
     super(
       Object.assign(options, {
diff --git a/src/trace/span/Span.ts b/src/trace/span/Span.ts
index 2899a45..a240c8a 100644
--- a/src/trace/span/Span.ts
+++ b/src/trace/span/Span.ts
@@ -50,6 +50,7 @@
   operation: string;
   layer = SpanLayer.UNKNOWN;
   component = Component.UNKNOWN;
+  depth = 0;
   inherit?: Component;
 
   readonly tags: Tag[] = [];
@@ -74,12 +75,15 @@
   }
 
   start(): void {
-    this.startTime = new Date().getTime();
-    this.context.start(this);
+    if (++this.depth === 1) {
+      this.startTime = new Date().getTime();
+      this.context.start(this);
+    }
   }
 
   stop(): void {
-    this.context.stop(this);
+    if (--this.depth === 0)
+      this.context.stop(this);
   }
 
   async(): void {
diff --git a/src/trace/span/StackedSpan.ts b/src/trace/span/StackedSpan.ts
deleted file mode 100644
index 77e3243..0000000
--- a/src/trace/span/StackedSpan.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*!
- *
- * 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 Span, { SpanCtorOptions } from '../../trace/span/Span';
-import { SpanType } from '../../proto/language-agent/Tracing_pb';
-
-export default class StackedSpan extends Span {
-  depth = 0;
-
-  constructor(options: SpanCtorOptions & { type: SpanType }) {
-    super(options);
-  }
-
-  start(): void {
-    if (++this.depth === 1) {
-      super.start();
-    }
-  }
-
-  stop(): void {
-    if (--this.depth === 0) {
-      super.stop();
-    }
-  }
-}