diff --git a/src/Agent.php b/src/Agent.php index ee2c70b3..dfd3b26f 100644 --- a/src/Agent.php +++ b/src/Agent.php @@ -12,7 +12,7 @@ class Agent { const VERSION = '1.0'; - const NAME = 'scoutapm-php'; + const NAME = 'scout-apm-php'; private $config; @@ -23,11 +23,20 @@ class Agent private $logger; + // Class that helps check incoming http paths vs. the configured ignore list + private $ignoredEndpoints; + + // If this request was marked as ignored + private $isIgnored; + public function __construct() { $this->config = new Config($this); $this->request = new Request($this); $this->logger = new NullLogger(); + + $this->ignoredEndpoints = new IgnoredEndpoints($this); + $this->isIgnored = false; } public function connect() @@ -98,11 +107,21 @@ public function getConfig() : Config */ public function startSpan(string $operation, float $overrideTimestamp = null) { + if ($this->request === null) { + // Must return a Span object to match API. This is a dummy span + // that is not ever used for anything. + return new Span($this, "Ignored", "ignored-request"); + } + return $this->request->startSpan($operation, $overrideTimestamp); } public function stopSpan() { + if ($this->request === null) { + return null; + } + $this->request->stopSpan(); } @@ -134,17 +153,49 @@ public function addContext(string $tag, $value) public function tagRequest(string $tag, $value) { + if ($this->request === null) { + return null; + } + return $this->request->tag($tag, $value); } + /* + * Check if a given URL was configured as ignored. + * Does not alter the running request. If you wish to abort tracing of this + * request, use ignore() + */ + public function ignored(string $path) : bool + { + return $this->ignoredEndpoints->ignored($path); + } + + /* + * Mark the running request as ignored. Triggers optimizations in various + * tracing and tagging methods to turn them into NOOPs + */ + public function ignore() + { + $this->request = null; + $this->isIgnored = true; + } + + // Returns true only if the request was sent onward to the core agent. + // False otherwise. public function send() : bool { + // Don't send if the agent is not enabled. if (! $this->enabled()) { - return true; + return false; } - $status = $this->connector->sendRequest($this->request); + // Don't send it if the request was ignored + if ($this->isIgnored) { + return false; + } + // Send this request off to the CoreAgent + $status = $this->connector->sendRequest($this->request); return $status; } diff --git a/src/Config.php b/src/Config.php index 5ae0c591..3322be54 100644 --- a/src/Config.php +++ b/src/Config.php @@ -28,6 +28,7 @@ public function __construct(\Scoutapm\Agent $agent) $this->coercions = [ "monitor" => Config\BoolCoercion::class, + "ignore" => Config\JSONCoercion::class, ]; } diff --git a/src/IgnoredEndpoints.php b/src/IgnoredEndpoints.php new file mode 100644 index 00000000..1308d5ea --- /dev/null +++ b/src/IgnoredEndpoints.php @@ -0,0 +1,35 @@ +agent = $agent; + $this->config = $agent->getConfig(); + } + + public function ignored(string $url) : bool + { + $ignored = $this->config->get("ignore"); + if ($ignored == null) { + return false; + } + + foreach ($ignored as $ignore) { + if (substr($url, 0, strlen($ignore)) === $ignore) { + return true; + } + } + + // None Matched + return false; + } +} diff --git a/tests/IgnoredEndpointsTest.php b/tests/IgnoredEndpointsTest.php new file mode 100644 index 00000000..9c581134 --- /dev/null +++ b/tests/IgnoredEndpointsTest.php @@ -0,0 +1,47 @@ +getConfig(); + $config->set("ignore", [ + "/health", + "/status", + ]); + $ignoredEndpoints = new IgnoredEndpoints($agent); + + // Exact Match + $this->assertEquals(true, $ignoredEndpoints->ignored("/health")); + $this->assertEquals(true, $ignoredEndpoints->ignored("/status")); + + // Prefix Match + $this->assertEquals(true, $ignoredEndpoints->ignored("/health/database")); + $this->assertEquals(true, $ignoredEndpoints->ignored("/status/time")); + + // No Match + $this->assertEquals(false, $ignoredEndpoints->ignored("/signup")); + + // Not-prefix doesn't Match + $this->assertEquals(false, $ignoredEndpoints->ignored("/hero/1/health")); + } + + public function testWorksWithNullIgnoreSetting() + { + $agent = new Agent(); + $config = $agent->getConfig(); + $ignoredEndpoints = new IgnoredEndpoints($agent); + + // No Match + $this->assertEquals(false, $ignoredEndpoints->ignored("/signup")); + } +} diff --git a/tests/Unit/AgentTest.php b/tests/Unit/AgentTest.php index ce8d43c4..2a467ddb 100644 --- a/tests/Unit/AgentTest.php +++ b/tests/Unit/AgentTest.php @@ -148,4 +148,43 @@ public function testEnabled() $this->assertEquals(true, $agent->enabled()); } + + public function testIgnoredEndpoints() + { + $agent = new Agent(); + $agent->getConfig()->set("ignore", ["/foo"]); + + $this->assertEquals(true, $agent->ignored("/foo")); + $this->assertEquals(false, $agent->ignored("/bar")); + } + + // Many instrumentation calls are NOOPs when ignore is called. Make sure + // the sequence works as expected + public function testIgnoredAgentSequence() + { + $agent = new Agent(); + $agent->ignore(); + + // Start a Parent Controller Span + $span = $agent->startSpan("Controller/Test"); + + // Tag Whole Request + $agent->tagRequest("uri", "example.com/foo/bar.php"); + + // Start a Child Span + $span = $agent->startSpan("SQL/Query"); + + // Tag the span + $span->tag("sql.query", "select * from foo"); + + // Finish Child Span + $agent->stopSpan(); + + // Stop Controller Span + $agent->stopSpan(); + + $agent->send(); + + $this->assertNotNull($agent); + } } diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index bfead0b6..f5f6e07e 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -47,4 +47,13 @@ public function testBooleanCoercionOfMonitor() $config->set("monitor", "true"); $this->assertSame(true, $config->get("monitor")); } + + public function testJSONCoercionOfIgnore() + { + $config = new Config(new Agent()); + + // Set a user config. This won't be looked up + $config->set("ignore", '["/foo", "/bar"]'); + $this->assertSame(["/foo", "/bar"], $config->get("ignore")); + } }