├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── breaking-changes.md ├── config.md ├── examples │ ├── agent-init.md │ ├── basic-usage.md │ ├── blob │ │ ├── dt_dashboard.png │ │ ├── kib_parent-transactions.png │ │ ├── kib_transactions.png │ │ ├── span_overview.png │ │ └── span_stacktrace.png │ ├── capture-throwable.md │ ├── convert-backtrace.md │ ├── distributed-tracing.md │ ├── metricset.php │ ├── parent-transactions.php │ ├── server-info.php │ └── spans.md ├── install.md └── knowledgebase.md ├── phpunit.xml.dist ├── src ├── Agent.php ├── Events │ ├── DefaultEventFactory.php │ ├── Error.php │ ├── EventBean.php │ ├── EventFactoryInterface.php │ ├── Metadata.php │ ├── Metricset.php │ ├── Span.php │ ├── TraceableEvent.php │ └── Transaction.php ├── Exception │ ├── InvalidTraceContextHeaderException.php │ ├── MissingAppNameException.php │ ├── Timer │ │ ├── AlreadyRunningException.php │ │ ├── NotStartedException.php │ │ └── NotStoppedException.php │ └── Transaction │ │ ├── DuplicateTransactionNameException.php │ │ └── UnknownTransactionException.php ├── Helper │ ├── Config.php │ ├── DistributedTracing.php │ ├── Encoding.php │ ├── StackTrace.php │ └── Timer.php ├── Middleware │ └── Connector.php ├── Stores │ ├── Store.php │ └── TransactionsStore.php └── Traits │ └── Events │ └── Stacktrace.php └── tests ├── AgentTest.php ├── Events └── TransactionTest.php ├── Helper ├── ConfigTest.php ├── DistributedTracingTest.php ├── EncodingTest.php └── TimerTest.php ├── PHPUnitUtils.php ├── Stores └── TransactionsStoreTest.php ├── TestCase.php ├── Traits └── Events │ └── StacktraceTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | phpunit.xml 4 | composer.phar 5 | .DS_STORE 6 | .idea 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --prefer-dist --no-interaction --no-suggest 11 | 12 | script: vendor/bin/phpunit -v 13 | 14 | notifications: 15 | on_success: never 16 | on_failure: always 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 philkra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elastic APM: PHP Agent 2 | 3 | [![Total Downloads](https://img.shields.io/packagist/dt/philkra/elastic-apm-php-agent.svg?style=flat)](https://packagist.org/packages/philkra/elastic-apm-php-agent) 4 | 5 | --- 6 | 7 | ⚠️ WARNING: This project is no longer maintained! The official Elastic PHP APM agent has superseeded this project. 8 | 9 | --- 10 | 11 | ## Contributors 12 | A big, big thank you goes out to every contributor of this repo, special thanks goes out to: 13 | * [dstepe](https://github.com/dstepe) 14 | * [georgeboot](https://github.com/georgeboot) 15 | * [alash3al](https://github.com/alash3al) 16 | * [thinkspill](https://github.com/thinkspill) 17 | * [YuZhenXie](https://github.com/YuZhenXie) 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "philkra/elastic-apm-php-agent", 3 | "description": "A php agent for Elastic APM v2 Intake API", 4 | "license": "MIT", 5 | "require" : { 6 | "php" : ">= 7.1", 7 | "guzzlehttp/guzzle": "6.*", 8 | "ralouphie/getallheaders": "3.*", 9 | "ext-curl" : "*", 10 | "ext-json": "*" 11 | }, 12 | "require-dev" : { 13 | "phpunit/phpunit" : "7.*" 14 | }, 15 | "suggest": { 16 | "ext-xdebug": "Required for processing of request headers" 17 | }, 18 | "autoload" : { 19 | "psr-4" : { 20 | "PhilKra\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "PhilKra\\Tests\\": "tests/" 26 | } 27 | }, 28 | "config" : { 29 | "optimize-autoloader" : true 30 | }, 31 | "authors" : [ 32 | { 33 | "name" : "Philip Krauss", 34 | "email" : "philip@philipkrauss.at", 35 | "homepage" : "https://github.com/philkra" 36 | }, 37 | { 38 | "name" : "George Boot", 39 | "email": "george@entryninja.com" 40 | }, 41 | { 42 | "name" : "Mohamed Al Ashaal", 43 | "email" : "mohamed.alashaal@speakol.com", 44 | "homepage" : "https://github.com/speakol-ads" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /docs/breaking-changes.md: -------------------------------------------------------------------------------- 1 | 2 | # Breaking Changes 3 | 4 | ## 6.x to 7.x 5 | * The `EventFactoryInterface` has been changed, in case you are injecting your custom Event Factory, you will be affected. 6 | * The methods `Transaction::setSpans`, `Transaction::getSpans`, `Transaction::getErrors` and `Transaction::setErrors` has been removed given the schema change rendered the these method unnecessary. 7 | * `Agent::__desctruct` triggers a flushing of the payload queue, you don't need to call `send()` manually anymore. 8 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The following parameters can be passed to the Agent in order to apply the required configuration. 4 | 5 | ``` 6 | appName : Name of this application, Required 7 | appVersion : Application version, Default: '' 8 | serverUrl : APM Server Endpoint, Default: 'http://127.0.0.1:8200' 9 | secretToken : Secret token for APM Server, Default: null 10 | hostname : Hostname to transmit to the APM Server, Default: gethostname() 11 | active : Activate the APM Agent, Default: true 12 | timeout : Guzzle Client timeout, Default: 5 13 | env : $_SERVER vars to send to the APM Server, empty set sends all. Keys are case sensitive, Default: ['SERVER_SOFTWARE'] 14 | cookies : Cookies to send to the APM Server, empty set sends all. Keys are case sensitive, Default: [] 15 | httpClient : Extended GuzzleHttp\Client Default: [] 16 | backtraceLimit: Depth of a transaction backtrace, Default: unlimited 17 | ``` 18 | 19 | Detailed `GuzzleHttp\Client` options can be found [here](http://docs.guzzlephp.org/en/stable/request-options.html#request-options). 20 | 21 | ## Example of an extended Configuration 22 | ```php 23 | $config = [ 24 | 'appName' => 'My WebApp', 25 | 'appVersion' => '1.0.42', 26 | 'serverUrl' => 'http://apm-server.example.com', 27 | 'secretToken' => 'DKKbdsupZWEEzYd4LX34TyHF36vDKRJP', 28 | 'hostname' => 'node-24.app.network.com', 29 | 'env' => ['DOCUMENT_ROOT', 'REMOTE_ADDR', 'REMOTE_USER'], 30 | 'cookies' => ['my-cookie'], 31 | 'httpClient' => [ 32 | 'verify' => false, 33 | 'proxy' => 'tcp://localhost:8125' 34 | ], 35 | ]; 36 | $agent = new \PhilKra\Agent($config); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/examples/agent-init.md: -------------------------------------------------------------------------------- 1 | # Initialize the Agent 2 | 3 | See all configuration options [here](https://github.com/philkra/elastic-apm-php-agent/blob/master/docs/config.md). 4 | 5 | ## With minimal Config 6 | ```php 7 | $agent = new \PhilKra\Agent( [ 'appName' => 'demo' ] ); 8 | ``` 9 | 10 | ## With elaborate Config 11 | When creating the agent, you can directly inject shared contexts such as user, tags and custom. 12 | ```php 13 | $agent = new \PhilKra\Agent( [ 'appName' => 'with-custom-context' ], [ 14 | 'user' => [ 15 | 'id' => 12345, 16 | 'email' => 'email@acme.com', 17 | ], 18 | 'tags' => [ 19 | // ... more key-values 20 | ], 21 | 'custom' => [ 22 | // ... more key-values 23 | ] 24 | ] ); 25 | ``` -------------------------------------------------------------------------------- /docs/examples/basic-usage.md: -------------------------------------------------------------------------------- 1 | # Transaction without minimal Meta data and Context 2 | ```php 3 | $transaction = $agent->startTransaction('Simple Transaction'); 4 | // Do some stuff you want to watch ... 5 | $agent->stopTransaction($transaction->getTransactionName()); 6 | ``` 7 | 8 | # Transaction with Meta data and Contexts 9 | ```php 10 | $trxName = 'Demo Transaction with more Data'; 11 | $agent->startTransaction( $trxName ); 12 | // Do some stuff you want to watch ... 13 | $agent->stopTransaction( $trxName, [ 14 | 'result' => '200', 15 | 'type' => 'demo' 16 | ] ); 17 | $agent->getTransaction( $trxName )->setUserContext( [ 18 | 'id' => 12345, 19 | 'email' => "hello@acme.com", 20 | ] ); 21 | $agent->getTransaction( $trxName )->setCustomContext( [ 22 | 'foo' => 'bar', 23 | 'bar' => [ 'foo1' => 'bar1', 'foo2' => 'bar2' ] 24 | ] ); 25 | $agent->getTransaction( $trxName )->setTags( [ 'k1' => 'v1', 'k2' => 'v2' ] ); 26 | ``` 27 | 28 | # Example of a Transaction 29 | This example illustrates how you can monitor a call to another web service. 30 | ```php 31 | $agent = new \PhilKra\Agent([ 'appName' => 'examples' ]); 32 | 33 | $endpoint = 'https://acme.com/api/'; 34 | $payload = [ 'foo' => 'bar' ]; 35 | $client = new GuzzleHttp\Client(); 36 | 37 | // Start the Transaction 38 | $transaction = $agent->startTransaction('POST https://acme.com/api/'); 39 | 40 | // Do the call via curl/Guzzle e.g. 41 | $response = $client->request('POST', $endpoint, [ 42 | 'json' => $payload 43 | ]); 44 | 45 | // Stop the Transaction tracing, attach the Status and the sent Payload 46 | $agent->stopTransaction($transaction->getTransactionName(), [ 47 | 'status' => $response->getStatusCode(), 48 | 'payload' => $payload, 49 | ]); 50 | 51 | // The collected traces will be send to the APM server as soon as 52 | // the Agent object is destroyed. But, you can manually flush the 53 | // playload queue with $agent->send(); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/examples/blob/dt_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philkra/elastic-apm-php-agent/b264aa833210bd79f729afeabfb343a3e8151bf5/docs/examples/blob/dt_dashboard.png -------------------------------------------------------------------------------- /docs/examples/blob/kib_parent-transactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philkra/elastic-apm-php-agent/b264aa833210bd79f729afeabfb343a3e8151bf5/docs/examples/blob/kib_parent-transactions.png -------------------------------------------------------------------------------- /docs/examples/blob/kib_transactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philkra/elastic-apm-php-agent/b264aa833210bd79f729afeabfb343a3e8151bf5/docs/examples/blob/kib_transactions.png -------------------------------------------------------------------------------- /docs/examples/blob/span_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philkra/elastic-apm-php-agent/b264aa833210bd79f729afeabfb343a3e8151bf5/docs/examples/blob/span_overview.png -------------------------------------------------------------------------------- /docs/examples/blob/span_stacktrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philkra/elastic-apm-php-agent/b264aa833210bd79f729afeabfb343a3e8151bf5/docs/examples/blob/span_stacktrace.png -------------------------------------------------------------------------------- /docs/examples/capture-throwable.md: -------------------------------------------------------------------------------- 1 | # Capture Errors and Exceptions 2 | The agent can capture all types or errors and exceptions that are implemented from the interface [`Throwable`](http://php.net/manual/en/class.throwable.php). When capturing an _error_, you can a context and highly recommended a parent `transaction` as illustrated in the following snippet. 3 | 4 | By doing so you increase the tracability of the error. 5 | 6 | ```php 7 | // Setup Agent 8 | $config = [ 9 | 'appName' => 'examples', 10 | 'appVersion' => '1.0.0-beta', 11 | ]; 12 | $agent = new Agent($config); 13 | 14 | // start a new transaction or use an existing one 15 | $transaction = $agent->startTransaction('Failing-Transaction'); 16 | 17 | try { 18 | // 19 | // do stuff that generates an Exception 20 | // 21 | } 22 | catch(Exception $e) { 23 | $agent->captureThrowable($e, [], $transaction); 24 | // handle Exception .. 25 | } 26 | 27 | // do some more stuff .. 28 | $agent->stopTransaction($transaction->getTransactionName()); 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/examples/convert-backtrace.md: -------------------------------------------------------------------------------- 1 | # Converting debug_backtrace to a stack trace 2 | 3 | There is a function on a span to set a stack trace but it uses a different format from PHP's `debug_backtrace()`. 4 | 5 | In order to convert between the two you can use the setDebugBacktrace function. It will convert details in the 6 | background and set the stack trace for you. 7 | 8 | A simple example would be: 9 | 10 | ```php 11 | $spanSt->setDebugBacktrace(); 12 | ``` 13 | 14 | ## Example Code 15 | ```php 16 | use PhilKra\Helper\StackTrace; 17 | 18 | // create the agent 19 | $agent = new \PhilKra\Agent(['appName' => 'examples']); 20 | 21 | $agent = new Agent($config); 22 | 23 | // Span 24 | // start a new transaction 25 | $parent = $agent->startTransaction('POST /auth/examples/spans'); 26 | 27 | // burn some time 28 | usleep(rand(10, 25)); 29 | 30 | // Create Span 31 | $spanParent = $agent->factory()->newSpan('Authenication Workflow', $parent); 32 | // $parent->incSpanCount(); 33 | $spanParent->start(); 34 | 35 | // Create another Span that is a parent span 36 | $spanSt = $agent->factory()->newSpan('Span with stacktrace', $spanParent); 37 | // $parent->incSpanCount(); 38 | $spanSt->start(); 39 | 40 | // burn some fictive time .. 41 | usleep(rand(250, 350)); 42 | $spanSt->setDebugBacktrace(); 43 | 44 | $spanSt->stop(); 45 | $agent->putEvent($spanSt); 46 | 47 | $spanParent->stop(); 48 | 49 | // Do some stuff you want to watch ... 50 | usleep(rand(100, 250)); 51 | 52 | $agent->putEvent($spanParent); 53 | 54 | $agent->stopTransaction($parent->getTransactionName()); 55 | 56 | // Force manual flush if needed 57 | // $agent->send(); 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /docs/examples/distributed-tracing.md: -------------------------------------------------------------------------------- 1 | # Distributed tracing 2 | Distributed tracing headers are automatically captured by transactions. 3 | Elastic's `elastic-apm-traceparent` and W3C's `traceparent` headers are both supported. 4 | 5 | This example illustrates the forward propagtion of the tracing Id, by showing how the invoked service queries another service. 6 | 7 | **TL:DR** Passing the distributed tracing id to another service, you need to add the header `elastic-apm-traceparent` with the value of `getDistributedTracing()` of a `Span` or a `Transaction`. 8 | 9 | ## Screenshots 10 | ![Dashboard](https://github.com/philkra/elastic-apm-php-agent/blob/master/docs/examples/blob/dt_dashboard.png "Distributed Tracing Dashboard") 11 | 12 | ## Example Code 13 | ```php 14 | // Setup Agent 15 | $config = [ 16 | 'appName' => 'examples', 17 | 'appVersion' => '1.0.0', 18 | ]; 19 | $agent = new Agent($config); 20 | 21 | // Wrap everything in a Parent transaction 22 | $parent = $agent->startTransaction('GET /data/12345'); 23 | $spanCache = $agent->factory()->newSpan('DB User Lookup', $parent); 24 | $spanCache->setType('db.redis.query'); 25 | $spanCache->start(); 26 | 27 | // do some db.mysql.query action .. 28 | usleep(rand(250, 450)); 29 | 30 | $spanCache->stop(); 31 | $spanCache->setContext(['db' => [ 32 | 'instance' => 'redis01.example.foo', 33 | 'statement' => 'GET data_12345', 34 | ]]); 35 | $agent->putEvent($spanCache); 36 | 37 | // Query microservice with Traceparent Header 38 | $spanHttp = $agent->factory()->newSpan('Query DataStore Service', $parent); 39 | $spanHttp->setType('external.http'); 40 | $spanHttp->start(); 41 | 42 | $url = 'http://127.0.0.1:5001'; 43 | $curl = curl_init(); 44 | curl_setopt_array($curl, [ 45 | CURLOPT_RETURNTRANSFER => 1, 46 | CURLOPT_URL => $url, 47 | CURLOPT_HTTPHEADER => [ 48 | sprintf('%s: %s', DistributedTracing::HEADER_NAME, $spanHttp->getDistributedTracing()), 49 | ], 50 | ]); 51 | $resp = curl_exec($curl); 52 | //$info = curl_getinfo($curl); 53 | 54 | $spanHttp->stop(); 55 | $spanHttp->setContext(['http' => [ 56 | 'instance' => $url, 57 | 'statement' => 'GET /', 58 | ]]); 59 | $agent->putEvent($spanHttp); 60 | 61 | // do something with the file 62 | $span = $agent->factory()->newSpan('do something', $parent); 63 | $span->start(); 64 | usleep(rand(2500, 3500)); 65 | $span->stop(); 66 | $agent->putEvent($span); 67 | 68 | $agent->stopTransaction($parent->getTransactionName()); 69 | ``` 70 | 71 | Big thanks to [samuelbednarcik](https://github.com/samuelbednarcik) because the idea comes from his [elastic-apm-php-agent](https://github.com/samuelbednarcik/elastic-apm-php-agent). -------------------------------------------------------------------------------- /docs/examples/metricset.php: -------------------------------------------------------------------------------- 1 | 'examples', 14 | 'appVersion' => '1.0.0-beta', 15 | ]; 16 | 17 | $agent = new Agent($config); 18 | $agent->putEvent($agent->factory()->newMetricset([ 19 | 'system.cpu.total.norm.pct' => min(sys_getloadavg()[0]/100, 1), 20 | ])); 21 | 22 | // more Events to trace .. 23 | -------------------------------------------------------------------------------- /docs/examples/parent-transactions.php: -------------------------------------------------------------------------------- 1 | 'examples', 11 | 'appVersion' => '1.0.0-beta', 12 | 'env' => ['REMOTE_ADDR'], 13 | ]; 14 | 15 | $agent = new Agent($config); 16 | 17 | // Start a new parent Transaction 18 | $parent = $agent->startTransaction('GET /users'); 19 | 20 | // Start a child Transaction and set the Parent 21 | $childOne = $agent->startTransaction('http.session.get.auth.data'); 22 | $childOne->setParent($parent); 23 | 24 | // Do stuff .. 25 | usleep(rand(1000, 99999)); 26 | 27 | $agent->stopTransaction($childOne->getTransactionName()); 28 | 29 | // Start another child Transaction and set the Parent 30 | $childTwo = $agent->startTransaction('elasticsearch.search.active.users'); 31 | $childTwo->setParent($parent); 32 | 33 | // Do stuff .. 34 | usleep(rand(1000, 99999)); 35 | 36 | $agent->stopTransaction($childTwo->getTransactionName()); 37 | 38 | // Create a 3rd child Transaction that is throwing an exception 39 | $childThree = $agent->startTransaction('division.by.zero'); 40 | $childThree->setParent($parent); 41 | $agent->captureThrowable( new Exception("division by 0 exception"), [], $childThree ); 42 | $agent->stopTransaction($childThree->getTransactionName()); 43 | 44 | $agent->stopTransaction($parent->getTransactionName()); 45 | 46 | // $agent->send(); 47 | -------------------------------------------------------------------------------- /docs/examples/server-info.php: -------------------------------------------------------------------------------- 1 | 'examples', 13 | 'appVersion' => '1.0.0-beta', 14 | ]; 15 | 16 | $agent = new Agent($config); 17 | 18 | $info = $agent->info(); 19 | 20 | var_dump($info->getStatusCode()); 21 | var_dump($info->getBody()); 22 | -------------------------------------------------------------------------------- /docs/examples/spans.md: -------------------------------------------------------------------------------- 1 | # Adding spans 2 | Please consult the documentation for your exact needs. 3 | Below is an example to add spans for MySQL, Redis and generic request wraped by a parent span. 4 | 5 | ## Screenshots 6 | **Transactions Dashboard showing the Spans** 7 | ![Dashboard](https://github.com/philkra/elastic-apm-php-agent/blob/master/docs/examples/blob/span_overview.png "Spans Dashboard") 8 | 9 | **Stacktrace of a Span** 10 | ![Stacktrace](https://github.com/philkra/elastic-apm-php-agent/blob/master/docs/examples/blob/span_stacktrace.png "Span Stacktrace") 11 | 12 | ## Example Code 13 | ```php 14 | // create the agent 15 | $agent = new \PhilKra\Agent(['appName' => 'examples']); 16 | 17 | $agent = new Agent($config); 18 | 19 | // Span 20 | // start a new transaction 21 | $parent = $agent->startTransaction('POST /auth/examples/spans'); 22 | 23 | // burn some time 24 | usleep(rand(10, 25)); 25 | 26 | // Create Span 27 | $spanParent = $agent->factory()->newSpan('Authenication Workflow', $parent); 28 | // $parent->incSpanCount(); 29 | $spanParent->start(); 30 | 31 | // Lookup the User 'foobar' in the database 32 | $spanDb = $agent->factory()->newSpan('DB User Lookup', $spanParent); 33 | // $parent->incSpanCount(); 34 | $spanDb->setType('db.mysql.query'); 35 | $spanDb->setAction('query'); 36 | $spanDb->start(); 37 | 38 | // do some db.mysql.query action .. 39 | usleep(rand(100, 300)); 40 | 41 | $spanDb->stop(); 42 | // Attach addition/optional Context 43 | $spanDb->setContext(['db' => [ 44 | 'instance' => 'my_database', // the database name 45 | 'statement' => 'select * from non_existing_table where username = "foobar"', // the query being executed 46 | 'type' => 'sql', 47 | 'user' => 'user', 48 | ]]); 49 | $agent->putEvent($spanDb); 50 | 51 | // Stach the record into Redis 52 | $spanCache = $agent->factory()->newSpan('DB User Lookup', $spanParent); 53 | // $parent->incSpanCount(); 54 | $spanCache->setType('db.redis.query'); 55 | $spanCache->setAction('query'); 56 | $spanCache->start(); 57 | 58 | 59 | // do some db.mysql.query action .. 60 | usleep(rand(10, 30)); 61 | 62 | $spanCache->stop(); 63 | $spanCache->setContext(['db' => [ 64 | 'instance' => 'redis01.example.foo', 65 | 'statement' => 'SET user_foobar "12345"', 66 | ]]); 67 | $agent->putEvent($spanCache); 68 | 69 | // Create another Span that is a parent span 70 | $spanHash = $agent->factory()->newSpan('Validate Credentials', $spanParent); 71 | // $parent->incSpanCount(); 72 | $spanHash->start(); 73 | 74 | // do some password hashing and matching .. 75 | usleep(rand(50, 100)); 76 | 77 | $spanHash->stop(); 78 | $agent->putEvent($spanHash); 79 | 80 | // Create another Span that is a parent span 81 | $spanSt = $agent->factory()->newSpan('Span with stacktrace', $spanParent); 82 | // $parent->incSpanCount(); 83 | $spanSt->start(); 84 | 85 | // burn some fictive time .. 86 | usleep(rand(250, 350)); 87 | $spanSt->setStackTrace([ 88 | [ 89 | 'function' => "\\YourOrMe\\Library\\Class::methodCall()", 90 | 'abs_path' => '/full/path/to/file.php', 91 | 'filename' => 'file.php', 92 | 'lineno' => 30, 93 | 'library_frame' => false, // indicated whether this code is 'owned' by an (external) library or not 94 | 'vars' => [ 95 | 'arg1' => 'value', 96 | 'arg2' => 'value2', 97 | ], 98 | 'pre_context' => [ // lines of code leading to the context line 99 | ' '$result = mysql_query("select * from non_existing_table")', // source code of context line 104 | 'post_context' => [// lines of code after to the context line 105 | '', 106 | '$table = $fakeTableBuilder->buildWithResult($result);', 107 | 'return $table;', 108 | ], 109 | ] 110 | ]); 111 | 112 | $spanSt->stop(); 113 | $agent->putEvent($spanSt); 114 | 115 | $spanParent->stop(); 116 | 117 | // Do some stuff you want to watch ... 118 | usleep(rand(100, 250)); 119 | 120 | $agent->putEvent($spanParent); 121 | 122 | $agent->stopTransaction($parent->getTransactionName()); 123 | 124 | // Force manual flush if needed 125 | // $agent->send(); 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | The recommended way to install the agent is through [Composer](http://getcomposer.org). 3 | 4 | Run the following composer command: 5 | 6 | ```bash 7 | php composer.phar require philkra/elastic-apm-php-agent 8 | ``` 9 | 10 | After installing, you need to require Composer's autoloader: 11 | 12 | ```php 13 | require 'vendor/autoload.php'; 14 | ``` -------------------------------------------------------------------------------- /docs/knowledgebase.md: -------------------------------------------------------------------------------- 1 | 2 | # Knowledgebase 3 | 4 | ## Disable Agent for CLI 5 | In case you want to disable the agent dynamically for hybrid SAPI usage, please use the following snippet. 6 | ```php 7 | 'active' => PHP_SAPI !== 'cli' 8 | ``` 9 | In case for the Laravel APM provider: 10 | ```php 11 | 'active' => PHP_SAPI !== 'cli' && env('ELASTIC_APM_ACTIVE', false) 12 | ``` 13 | Thank you to @jblotus, (https://github.com/philkra/elastic-apm-laravel/issues/19) -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | 17 | ./src/ 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Agent.php: -------------------------------------------------------------------------------- 1 | [], 70 | 'custom' => [], 71 | 'tags' => [] 72 | ]; 73 | 74 | /** 75 | * @var EventFactoryInterface 76 | */ 77 | private $eventFactory; 78 | 79 | /** 80 | * @var Connector 81 | */ 82 | private $connector; 83 | 84 | /** 85 | * Setup the APM Agent 86 | * 87 | * @param array $config 88 | * @param array $sharedContext Set shared contexts such as user and tags 89 | * @param EventFactoryInterface $eventFactory Alternative factory to use when creating event objects 90 | * 91 | * @return void 92 | */ 93 | public function __construct(array $config, array $sharedContext = [], EventFactoryInterface $eventFactory = null, TransactionsStore $transactionsStore = null) 94 | { 95 | // Init Agent Config 96 | $this->config = new Config($config); 97 | 98 | // Use the custom event factory or create a default one 99 | $this->eventFactory = $eventFactory ?? new DefaultEventFactory(); 100 | 101 | // Init the Shared Context 102 | $this->sharedContext['user'] = $sharedContext['user'] ?? []; 103 | $this->sharedContext['custom'] = $sharedContext['custom'] ?? []; 104 | $this->sharedContext['tags'] = $sharedContext['tags'] ?? []; 105 | 106 | // Let's misuse the context to pass the environment variable and cookies 107 | // config to the EventBeans and the getContext method 108 | // @see https://github.com/philkra/elastic-apm-php-agent/issues/27 109 | // @see https://github.com/philkra/elastic-apm-php-agent/issues/30 110 | $this->sharedContext['env'] = $this->config->get('env', []); 111 | $this->sharedContext['cookies'] = $this->config->get('cookies', []); 112 | 113 | // Initialize Event Stores 114 | $this->transactionsStore = $transactionsStore ?? new TransactionsStore(); 115 | 116 | // Init the Transport "Layer" 117 | $this->connector = new Connector($this->config); 118 | $this->connector->putEvent(new Metadata([], $this->config)); 119 | 120 | // Start Global Agent Timer 121 | $this->timer = new Timer(); 122 | $this->timer->start(); 123 | } 124 | 125 | /** 126 | * Event Factory 127 | * 128 | * @return EventFactoryInterface 129 | */ 130 | public function factory() : EventFactoryInterface 131 | { 132 | return $this->eventFactory; 133 | } 134 | 135 | /** 136 | * Query the Info endpoint of the APM Server 137 | * 138 | * @link https://www.elastic.co/guide/en/apm/server/7.3/server-info.html 139 | * 140 | * @return Response 141 | */ 142 | public function info() : \GuzzleHttp\Psr7\Response 143 | { 144 | return $this->connector->getInfo(); 145 | } 146 | 147 | /** 148 | * Start the Transaction capturing 149 | * 150 | * @throws \PhilKra\Exception\Transaction\DuplicateTransactionNameException 151 | * 152 | * @param string $name 153 | * @param array $context 154 | * 155 | * @return Transaction 156 | */ 157 | public function startTransaction(string $name, array $context = [], float $start = null): Transaction 158 | { 159 | // Create and Store Transaction 160 | $this->transactionsStore->register( 161 | $this->factory()->newTransaction($name, array_replace_recursive($this->sharedContext, $context), $start) 162 | ); 163 | 164 | // Start the Transaction 165 | $transaction = $this->transactionsStore->fetch($name); 166 | 167 | if (null === $start) { 168 | $transaction->start(); 169 | } 170 | 171 | return $transaction; 172 | } 173 | 174 | /** 175 | * Stop the Transaction 176 | * 177 | * @throws \PhilKra\Exception\Transaction\UnknownTransactionException 178 | * 179 | * @param string $name 180 | * @param array $meta, Def: [] 181 | * 182 | * @return void 183 | */ 184 | public function stopTransaction(string $name, array $meta = []) 185 | { 186 | $this->getTransaction($name)->setBacktraceLimit($this->config->get('backtraceLimit', 0)); 187 | $this->getTransaction($name)->stop(); 188 | $this->getTransaction($name)->setMeta($meta); 189 | } 190 | 191 | /** 192 | * Get a Transaction 193 | * 194 | * @throws \PhilKra\Exception\Transaction\UnknownTransactionException 195 | * 196 | * @param string $name 197 | * 198 | * @return Transaction 199 | */ 200 | public function getTransaction(string $name) 201 | { 202 | $transaction = $this->transactionsStore->fetch($name); 203 | if ($transaction === null) { 204 | throw new UnknownTransactionException($name); 205 | } 206 | 207 | return $transaction; 208 | } 209 | 210 | /** 211 | * Register a Thrown Exception, Error, etc. 212 | * 213 | * @link http://php.net/manual/en/class.throwable.php 214 | * 215 | * @param \Throwable $thrown 216 | * @param array $context, Def: [] 217 | * @param Transaction $parent, Def: null 218 | * 219 | * @return void 220 | */ 221 | public function captureThrowable(\Throwable $thrown, array $context = [], ?Transaction $parent = null) 222 | { 223 | $this->putEvent($this->factory()->newError($thrown, array_replace_recursive($this->sharedContext, $context), $parent)); 224 | } 225 | 226 | /** 227 | * Put an Event to the Events Pool 228 | */ 229 | public function putEvent(EventBean $event) 230 | { 231 | $this->connector->putEvent($event); 232 | } 233 | 234 | /** 235 | * Get the Agent Config 236 | * 237 | * @return \PhilKra\Helper\Config 238 | */ 239 | public function getConfig() : \PhilKra\Helper\Config 240 | { 241 | return $this->config; 242 | } 243 | 244 | /** 245 | * Send Data to APM Service 246 | * 247 | * @link https://github.com/philkra/elastic-apm-laravel/issues/22 248 | * @link https://github.com/philkra/elastic-apm-laravel/issues/26 249 | * 250 | * @return bool 251 | */ 252 | public function send() : bool 253 | { 254 | // Is the Agent enabled ? 255 | if ($this->config->get('active') === false) { 256 | $this->transactionsStore->reset(); 257 | return true; 258 | } 259 | 260 | // Put the preceding Metadata 261 | // TODO -- add context ? 262 | if($this->connector->isPayloadSet() === false) { 263 | $this->putEvent(new Metadata([], $this->config)); 264 | } 265 | 266 | // Start Payload commitment 267 | foreach($this->transactionsStore->list() as $event) { 268 | $this->connector->putEvent($event); 269 | } 270 | $this->transactionsStore->reset(); 271 | return $this->connector->commit(); 272 | } 273 | 274 | /** 275 | * Flush the Queue Payload 276 | * 277 | * @link https://www.php.net/manual/en/language.oop5.decon.php#object.destruct 278 | */ 279 | function __destruct() { 280 | $this->send(); 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /src/Events/DefaultEventFactory.php: -------------------------------------------------------------------------------- 1 | throwable = $throwable; 36 | } 37 | 38 | /** 39 | * Serialize Error Event 40 | * 41 | * @return array 42 | */ 43 | public function jsonSerialize() : array 44 | { 45 | return [ 46 | 'error' => [ 47 | 'id' => $this->getId(), 48 | 'transaction_id' => $this->getParentId(), 49 | 'parent_id' => $this->getParentId(), 50 | 'trace_id' => $this->getTraceId(), 51 | 'timestamp' => $this->getTimestamp(), 52 | 'context' => $this->getContext(), 53 | 'culprit' => Encoding::keywordField(sprintf('%s:%d', $this->throwable->getFile(), $this->throwable->getLine())), 54 | 'exception' => [ 55 | 'message' => $this->throwable->getMessage(), 56 | 'type' => Encoding::keywordField(get_class($this->throwable)), 57 | 'code' => $this->throwable->getCode(), 58 | 'stacktrace' => $this->mapStacktrace(), 59 | ], 60 | ] 61 | ]; 62 | } 63 | 64 | /** 65 | * Map the Stacktrace to Schema 66 | * 67 | * @return array 68 | */ 69 | final private function mapStacktrace() : array 70 | { 71 | $stacktrace = []; 72 | 73 | foreach ($this->throwable->getTrace() as $trace) { 74 | $item = [ 75 | 'function' => $trace['function'] ?? '(closure)' 76 | ]; 77 | 78 | if (isset($trace['line']) === true) { 79 | $item['lineno'] = $trace['line']; 80 | } 81 | 82 | if (isset($trace['file']) === true) { 83 | $item += [ 84 | 'filename' => basename($trace['file']), 85 | 'abs_path' => $trace['file'] 86 | ]; 87 | } 88 | 89 | if (isset($trace['class']) === true) { 90 | $item['module'] = $trace['class']; 91 | } 92 | if (isset($trace['type']) === true) { 93 | $item['type'] = $trace['type']; 94 | } 95 | 96 | if (!isset($item['lineno'])) { 97 | $item['lineno'] = 0; 98 | } 99 | 100 | if (!isset($item['filename'])) { 101 | $item['filename'] = '(anonymous)'; 102 | } 103 | 104 | array_push($stacktrace, $item); 105 | } 106 | 107 | return $stacktrace; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Events/EventBean.php: -------------------------------------------------------------------------------- 1 | 200, 59 | 'type' => 'generic' 60 | ]; 61 | 62 | /** 63 | * Extended Contexts such as Custom and/or User 64 | * 65 | * @var array 66 | */ 67 | private $contexts = [ 68 | 'request' => [], 69 | 'user' => [], 70 | 'custom' => [], 71 | 'env' => [], 72 | 'tags' => [], 73 | 'response' => [ 74 | 'finished' => true, 75 | 'headers_sent' => true, 76 | 'status_code' => 200, 77 | ], 78 | ]; 79 | 80 | /** 81 | * Init the Event with the Timestamp and UUID 82 | * 83 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/3 84 | * 85 | * @param array $contexts 86 | * @param ?Transaction $parent 87 | */ 88 | public function __construct(array $contexts, ?Transaction $parent = null) 89 | { 90 | // Generate Random Event Id 91 | $this->id = self::generateRandomBitsInHex(self::EVENT_ID_BITS); 92 | 93 | // Merge Initial Context 94 | $this->contexts = array_merge($this->contexts, $contexts); 95 | 96 | // Get current Unix timestamp with seconds 97 | $this->timestamp = round(microtime(true) * 1000000); 98 | 99 | // Set Parent Transaction 100 | if ($parent !== null) { 101 | $this->setParent($parent); 102 | } 103 | } 104 | 105 | /** 106 | * Get the Event Id 107 | * 108 | * @return string 109 | */ 110 | public function getId() : string 111 | { 112 | return $this->id; 113 | } 114 | 115 | /** 116 | * Get the Trace Id 117 | * 118 | * @return string $traceId 119 | */ 120 | public function getTraceId() : ?string 121 | { 122 | return $this->traceId; 123 | } 124 | 125 | /** 126 | * Set the Trace Id 127 | * 128 | * @param string $traceId 129 | */ 130 | final public function setTraceId(string $traceId) 131 | { 132 | $this->traceId = $traceId; 133 | } 134 | 135 | /** 136 | * Set the Parent Id 137 | * 138 | * @param string $parentId 139 | */ 140 | final public function setParentId(string $parentId) 141 | { 142 | $this->parentId = $parentId; 143 | } 144 | 145 | /** 146 | * Get the Parent Id 147 | * 148 | * @return string 149 | */ 150 | final public function getParentId() : ?string 151 | { 152 | return $this->parentId; 153 | } 154 | 155 | /** 156 | * Get the Offset between the Parent's timestamp and this Event's 157 | * 158 | * @return int 159 | */ 160 | final public function getParentTimestampOffset(): ?int 161 | { 162 | return $this->parentTimestampOffset; 163 | } 164 | 165 | /** 166 | * Get the Event's Timestamp 167 | * 168 | * @return int 169 | */ 170 | public function getTimestamp() : int 171 | { 172 | return $this->timestamp; 173 | } 174 | 175 | /** 176 | * Set the Parent Id and Trace Id based on the Parent Event 177 | * 178 | * @link https://www.elastic.co/guide/en/apm/server/current/transaction-api.html 179 | * 180 | * @param EventBean $parent 181 | */ 182 | public function setParent(EventBean $parent) 183 | { 184 | $this->setParentId($parent->getId()); 185 | $this->setTraceId($parent->getTraceId()); 186 | } 187 | 188 | /** 189 | * Set the Transaction Meta data 190 | * 191 | * @param array $meta 192 | * 193 | * @return void 194 | */ 195 | final public function setMeta(array $meta) 196 | { 197 | $this->meta = array_merge($this->meta, $meta); 198 | } 199 | 200 | /** 201 | * Set Meta data of User Context 202 | * 203 | * @param array $userContext 204 | */ 205 | final public function setUserContext(array $userContext) 206 | { 207 | $this->contexts['user'] = array_merge($this->contexts['user'], $userContext); 208 | } 209 | 210 | /** 211 | * Set custom Meta data for the Transaction in Context 212 | * 213 | * @param array $customContext 214 | */ 215 | final public function setCustomContext(array $customContext) 216 | { 217 | $this->contexts['custom'] = array_merge($this->contexts['custom'], $customContext); 218 | } 219 | 220 | /** 221 | * Set Transaction Response 222 | * 223 | * @param array $response 224 | */ 225 | final public function setResponse(array $response) 226 | { 227 | $this->contexts['response'] = array_merge($this->contexts['response'], $response); 228 | } 229 | 230 | /** 231 | * Set Tags for this Transaction 232 | * 233 | * @param array $tags 234 | */ 235 | final public function setTags(array $tags) 236 | { 237 | $this->contexts['tags'] = array_merge($this->contexts['tags'], $tags); 238 | } 239 | 240 | /** 241 | * Set Transaction Request 242 | * 243 | * @param array $request 244 | */ 245 | final public function setRequest(array $request) 246 | { 247 | $this->contexts['request'] = array_merge($this->contexts['request'], $request); 248 | } 249 | 250 | /** 251 | * Generate request data 252 | * 253 | * @return array 254 | */ 255 | final public function generateRequest(): array 256 | { 257 | $headers = getallheaders(); 258 | $http_or_https = isset($_SERVER['HTTPS']) ? 'https' : 'http'; 259 | 260 | // Build Context Stub 261 | $SERVER_PROTOCOL = $_SERVER['SERVER_PROTOCOL'] ?? ''; 262 | $remote_address = $_SERVER['REMOTE_ADDR'] ?? ''; 263 | if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) === true) { 264 | $remote_address = $_SERVER['HTTP_X_FORWARDED_FOR']; 265 | } 266 | 267 | return [ 268 | 'http_version' => substr($SERVER_PROTOCOL, strpos($SERVER_PROTOCOL, '/')), 269 | 'method' => $_SERVER['REQUEST_METHOD'] ?? 'cli', 270 | 'socket' => [ 271 | 'remote_address' => $remote_address, 272 | 'encrypted' => isset($_SERVER['HTTPS']) 273 | ], 274 | 'response' => $this->contexts['response'], 275 | 'url' => [ 276 | 'protocol' => $http_or_https, 277 | 'hostname' => Encoding::keywordField($_SERVER['SERVER_NAME'] ?? ''), 278 | 'port' => $_SERVER['SERVER_PORT'] ?? null, 279 | 'pathname' => Encoding::keywordField($_SERVER['SCRIPT_NAME'] ?? ''), 280 | 'search' => Encoding::keywordField('?' . (($_SERVER['QUERY_STRING'] ?? '') ?? '')), 281 | 'full' => Encoding::keywordField(isset($_SERVER['HTTP_HOST']) ? $http_or_https . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] : ''), 282 | ], 283 | 'headers' => [ 284 | 'user-agent' => $headers['User-Agent'] ?? '', 285 | 'cookie' => $this->getCookieHeader($headers['Cookie'] ?? ''), 286 | ], 287 | 'env' => (object)$this->getEnv(), 288 | 'cookies' => (object)$this->getCookies(), 289 | ]; 290 | } 291 | 292 | /** 293 | * Generate random bits in hexadecimal representation 294 | * 295 | * @param int $bits 296 | * @return string 297 | * @throws \Exception 298 | */ 299 | final protected function generateRandomBitsInHex(int $bits): string 300 | { 301 | return bin2hex(random_bytes($bits/8)); 302 | } 303 | 304 | /** 305 | * Get Type defined in Meta 306 | * 307 | * @return string 308 | */ 309 | final protected function getMetaType() : string 310 | { 311 | return $this->meta['type']; 312 | } 313 | 314 | /** 315 | * Get the Result of the Event from the Meta store 316 | * 317 | * @return string 318 | */ 319 | final protected function getMetaResult() : string 320 | { 321 | return (string)$this->meta['result']; 322 | } 323 | 324 | /** 325 | * Get the Environment Variables 326 | * 327 | * @link http://php.net/manual/en/reserved.variables.server.php 328 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/27 329 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/54 330 | * 331 | * @return array 332 | */ 333 | final protected function getEnv() : array 334 | { 335 | $envMask = $this->contexts['env']; 336 | $env = empty($envMask) 337 | ? $_SERVER 338 | : array_intersect_key($_SERVER, array_flip($envMask)); 339 | 340 | return $env; 341 | } 342 | 343 | /** 344 | * Get the cookies 345 | * 346 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/30 347 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/54 348 | * 349 | * @return array 350 | */ 351 | final protected function getCookies() : array 352 | { 353 | $cookieMask = $this->contexts['cookies'] ?? []; 354 | return empty($cookieMask) 355 | ? $_COOKIE 356 | : array_intersect_key($_COOKIE, array_flip($cookieMask)); 357 | } 358 | 359 | /** 360 | * Get the cookie header 361 | * 362 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/30 363 | * 364 | * @return string 365 | */ 366 | final protected function getCookieHeader(string $cookieHeader) : string 367 | { 368 | $cookieMask = $this->contexts['cookies'] ?? []; 369 | 370 | // Returns an empty string if cookies are masked. 371 | return empty($cookieMask) ? $cookieHeader : ''; 372 | } 373 | 374 | /** 375 | * Get the Events Context 376 | * 377 | * @link https://www.elastic.co/guide/en/apm/server/current/transaction-api.html#transaction-context-schema 378 | * 379 | * @return array 380 | */ 381 | final protected function getContext() : array 382 | { 383 | $context = [ 384 | 'request' => empty($this->contexts['request']) ? $this->generateRequest() : $this->contexts['request'] 385 | ]; 386 | 387 | // Add User Context 388 | if (empty($this->contexts['user']) === false) { 389 | $context['user'] = $this->contexts['user']; 390 | } 391 | 392 | // Add Custom Context 393 | if (empty($this->contexts['custom']) === false) { 394 | $context['custom'] = $this->contexts['custom']; 395 | } 396 | 397 | // Add Tags Context 398 | if (empty($this->contexts['tags']) === false) { 399 | $context['tags'] = $this->contexts['tags']; 400 | } 401 | 402 | return $context; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/Events/EventFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 89] 44 | * @param array $tags, Default [] 45 | * 46 | * @return Metricset 47 | */ 48 | public function newMetricset(array $set, array $tags = []): Metricset; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Events/Metadata.php: -------------------------------------------------------------------------------- 1 | config = $config; 32 | } 33 | 34 | /** 35 | * Generate request data 36 | * 37 | * @return array 38 | */ 39 | final public function jsonSerialize(): array 40 | { 41 | return [ 42 | 'metadata' => [ 43 | 'service' => [ 44 | 'name' => Encoding::keywordField($this->config->get('appName')), 45 | 'version' => Encoding::keywordField($this->config->get('appVersion')), 46 | 'framework' => [ 47 | 'name' => $this->config->get('framework') ?? '', 48 | 'version' => $this->config->get('frameworkVersion') ?? '', 49 | ], 50 | 'language' => [ 51 | 'name' => 'php', 52 | 'version' => phpversion() 53 | ], 54 | 'process' => [ 55 | 'pid' => getmypid(), 56 | ], 57 | 'agent' => [ 58 | 'name' => Agent::NAME, 59 | 'version' => Agent::VERSION 60 | ], 61 | 'environment' => Encoding::keywordField($this->config->get('environment')) 62 | ], 63 | 'system' => [ 64 | 'hostname' => Encoding::keywordField($this->config->get('hostname')), 65 | 'architecture' => php_uname('m'), 66 | 'platform' => php_uname('s') 67 | ] 68 | ] 69 | ]; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Events/Metricset.php: -------------------------------------------------------------------------------- 1 | $v) { 33 | $this->samples[$k] = ['value' => $v]; 34 | } 35 | $this->tags = $tags; 36 | } 37 | 38 | /** 39 | * Serialize Metricset 40 | * 41 | * TODO -- add tags 42 | * 43 | * @return array 44 | */ 45 | public function jsonSerialize() : array 46 | { 47 | return [ 48 | 'metricset' => [ 49 | 'samples' => $this->samples, 50 | // 'tags' => $this->tags, 51 | 'timestamp' => $this->getTimestamp(), 52 | ] 53 | ]; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Events/Span.php: -------------------------------------------------------------------------------- 1 | name = trim($name); 58 | $this->timer = new Timer(); 59 | $this->setParent($parent); 60 | } 61 | 62 | /** 63 | * Start the Timer 64 | * 65 | * @return void 66 | */ 67 | public function start() 68 | { 69 | $this->timer->start(); 70 | } 71 | 72 | /** 73 | * Stop the Timer 74 | * 75 | * @param integer|null $duration 76 | * 77 | * @return void 78 | */ 79 | public function stop(int $duration = null) 80 | { 81 | $this->timer->stop(); 82 | $this->duration = $duration ?? round($this->timer->getDurationInMilliseconds(), 3); 83 | } 84 | 85 | /** 86 | * Get the Event Name 87 | * 88 | * @return string 89 | */ 90 | public function getName() : string 91 | { 92 | return $this->name; 93 | } 94 | 95 | /** 96 | * Set the Span's Type 97 | * 98 | * @param string $action 99 | */ 100 | public function setAction(string $action) 101 | { 102 | $this->action = trim($action); 103 | } 104 | 105 | /** 106 | * Set the Spans' Action 107 | * 108 | * @param string $type 109 | */ 110 | public function setType(string $type) 111 | { 112 | $this->type = trim($type); 113 | } 114 | 115 | /** 116 | * Set a complimentary Stacktrace for the Span 117 | * 118 | * @link https://www.elastic.co/guide/en/apm/server/master/span-api.html 119 | * 120 | * @param array $stacktrace 121 | */ 122 | public function setStacktrace(array $stacktrace) 123 | { 124 | $this->stacktrace = $stacktrace; 125 | } 126 | 127 | /** 128 | * Serialize Span Event 129 | * 130 | * @link https://www.elastic.co/guide/en/apm/server/master/span-api.html 131 | * 132 | * @return array 133 | */ 134 | public function jsonSerialize() : array 135 | { 136 | return [ 137 | 'span' => [ 138 | 'id' => $this->getId(), 139 | 'transaction_id' => $this->getParentId(), 140 | 'trace_id' => $this->getTraceId(), 141 | 'parent_id' => $this->getParentId(), 142 | 'type' => Encoding::keywordField($this->type), 143 | 'action' => Encoding::keywordField($this->action), 144 | 'context' => $this->getContext(), 145 | 'duration' => $this->duration, 146 | 'name' => Encoding::keywordField($this->getName()), 147 | 'stacktrace' => $this->stacktrace, 148 | 'sync' => false, 149 | 'timestamp' => $this->getTimestamp(), 150 | ] 151 | ]; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Events/TraceableEvent.php: -------------------------------------------------------------------------------- 1 | setTraceContext(); 26 | } 27 | 28 | /** 29 | * Get the Distributed Tracing Value of this Event 30 | * 31 | * @return string 32 | */ 33 | public function getDistributedTracing() : string 34 | { 35 | return (new DistributedTracing($this->getTraceId(), $this->getParentId()))->__toString(); 36 | } 37 | 38 | /** 39 | * Set Trace context 40 | * 41 | * @throws \Exception 42 | */ 43 | private function setTraceContext() 44 | { 45 | // Is one of the Traceparent Headers populated ? 46 | $header = $_SERVER['HTTP_ELASTIC_APM_TRACEPARENT'] ?? ($_SERVER['HTTP_TRACEPARENT'] ?? null); 47 | if ($header !== null && DistributedTracing::isValidHeader($header) === true) { 48 | try { 49 | $traceParent = DistributedTracing::createFromHeader($header); 50 | 51 | $this->setTraceId($traceParent->getTraceId()); 52 | $this->setParentId($traceParent->getParentId()); 53 | } 54 | catch (InvalidTraceContextHeaderException $e) { 55 | $this->setTraceId(self::generateRandomBitsInHex(self::TRACE_ID_BITS)); 56 | } 57 | } 58 | else { 59 | $this->setTraceId(self::generateRandomBitsInHex(self::TRACE_ID_BITS)); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Events/Transaction.php: -------------------------------------------------------------------------------- 1 | 0.0, 36 | 'backtrace' => null, 37 | 'headers' => [] 38 | ]; 39 | 40 | /** 41 | * @var int 42 | */ 43 | private $backtraceLimit = 0; 44 | 45 | /** 46 | * Create the Transaction 47 | * 48 | * @param string $name 49 | * @param array $contexts 50 | */ 51 | public function __construct(string $name, array $contexts, $start = null) 52 | { 53 | parent::__construct($contexts); 54 | $this->setTransactionName($name); 55 | $this->timer = new Timer($start); 56 | } 57 | 58 | /** 59 | * Start the Transaction 60 | * 61 | * @return void 62 | */ 63 | public function start() 64 | { 65 | $this->timer->start(); 66 | } 67 | 68 | /** 69 | * Stop the Transaction 70 | * 71 | * @param integer|null $duration 72 | * 73 | * @return void 74 | */ 75 | public function stop(int $duration = null) 76 | { 77 | // Stop the Timer 78 | $this->timer->stop(); 79 | 80 | // Store Summary 81 | $this->summary['duration'] = $duration ?? round($this->timer->getDurationInMilliseconds(), 3); 82 | $this->summary['headers'] = (function_exists('xdebug_get_headers') === true) ? xdebug_get_headers() : []; 83 | $this->summary['backtrace'] = debug_backtrace($this->backtraceLimit); 84 | } 85 | 86 | /** 87 | * Set the Transaction Name 88 | * 89 | * @param string $name 90 | * 91 | * @return void 92 | */ 93 | public function setTransactionName(string $name) 94 | { 95 | $this->name = $name; 96 | } 97 | 98 | /** 99 | * Get the Transaction Name 100 | * 101 | * @return string 102 | */ 103 | public function getTransactionName() : string 104 | { 105 | return $this->name; 106 | } 107 | 108 | /** 109 | * Get the Summary of this Transaction 110 | * 111 | * @return array 112 | */ 113 | public function getSummary() : array 114 | { 115 | return $this->summary; 116 | } 117 | 118 | /** 119 | * Set the Max Depth/Limit of the debug_backtrace method 120 | * 121 | * @link http://php.net/manual/en/function.debug-backtrace.php 122 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/55 123 | * 124 | * @param int $limit 125 | */ 126 | public function setBacktraceLimit(int $limit) 127 | { 128 | $this->backtraceLimit = $limit; 129 | } 130 | 131 | /** 132 | * Serialize Transaction Event 133 | * 134 | * @return array 135 | */ 136 | public function jsonSerialize() : array 137 | { 138 | return [ 139 | 'transaction' => [ 140 | 'trace_id' => $this->getTraceId(), 141 | 'id' => $this->getId(), 142 | 'parent_id' => $this->getParentId(), 143 | 'type' => Encoding::keywordField($this->getMetaType()), 144 | 'duration' => $this->summary['duration'], 145 | 'timestamp' => $this->getTimestamp(), 146 | 'result' => $this->getMetaResult(), 147 | 'name' => Encoding::keywordField($this->getTransactionName()), 148 | 'context' => $this->getContext(), 149 | 'sampled' => null, 150 | 'span_count' => [ 151 | 'started' => 0, 152 | 'dropped' => 0, 153 | ], 154 | ] 155 | ]; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Exception/InvalidTraceContextHeaderException.php: -------------------------------------------------------------------------------- 1 | config = array_merge($this->getDefaultConfig(), $config); 32 | $this->config['serverUrl'] = rtrim ($this->config['serverUrl'], "/"); 33 | } 34 | 35 | /** 36 | * Get Config Value 37 | * 38 | * @param string $key 39 | * @param mixed $default 40 | * 41 | * @return mixed: value | null 42 | */ 43 | public function get(string $key, $default = null) 44 | { 45 | return ($this->config[$key]) ?? $default; 46 | } 47 | 48 | /** 49 | * Get the all Config Set as array 50 | * 51 | * @return array 52 | */ 53 | public function asArray() : array 54 | { 55 | return $this->config; 56 | } 57 | 58 | /** 59 | * Get the Default Config of the Agent 60 | * 61 | * @link https://github.com/philkra/elastic-apm-php-agent/issues/55 62 | * 63 | * @return array 64 | */ 65 | private function getDefaultConfig() : array 66 | { 67 | return [ 68 | 'serverUrl' => 'http://127.0.0.1:8200', 69 | 'secretToken' => null, 70 | 'hostname' => gethostname(), 71 | 'appVersion' => '', 72 | 'active' => true, 73 | 'timeout' => 10, 74 | 'env' => ['SERVER_SOFTWARE'], 75 | 'cookies' => [], 76 | 'httpClient' => [], 77 | 'environment' => 'development', 78 | 'backtraceLimit' => 0, 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Helper/DistributedTracing.php: -------------------------------------------------------------------------------- 1 | traceId = $traceId; 45 | $this->parentId = $parentId; 46 | $this->traceFlags = $traceFlags; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getTraceId() 53 | { 54 | return $this->traceId; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getParentId() 61 | { 62 | return $this->parentId; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getTraceFlags() : string 69 | { 70 | return $this->traceFlags; 71 | } 72 | 73 | /** 74 | * @param string $traceFlags 75 | */ 76 | public function setTraceFlags(string $traceFlags) 77 | { 78 | $this->traceFlags = $traceFlags; 79 | } 80 | 81 | /** 82 | * Check if the Header Value is valid 83 | * 84 | * @link https://www.w3.org/TR/trace-context/#version-format 85 | * 86 | * @param string $header 87 | * 88 | * @return bool 89 | */ 90 | public static function isValidHeader(string $header) : bool 91 | { 92 | return preg_match('/^'.self::VERSION.'-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/', $header) === 1; 93 | } 94 | 95 | /** 96 | * @param string $header 97 | * @return TraceParent 98 | * @throws InvalidTraceContextHeaderException 99 | */ 100 | public static function createFromHeader(string $header) 101 | { 102 | if (!self::isValidHeader($header)) { 103 | throw new InvalidTraceContextHeaderException("InvalidTraceContextHeaderException"); 104 | } 105 | $parsed = explode('-', $header); 106 | return new self($parsed[1], $parsed[2], $parsed[3]); 107 | } 108 | 109 | /** 110 | * Get Distributed Tracing Id 111 | * 112 | * @return string 113 | */ 114 | public function __toString() 115 | { 116 | return sprintf( 117 | '%s-%s-%s-%s', 118 | self::VERSION, 119 | $this->getTraceId(), 120 | $this->getParentId(), 121 | $this->getTraceFlags() 122 | ); 123 | } 124 | } -------------------------------------------------------------------------------- /src/Helper/Encoding.php: -------------------------------------------------------------------------------- 1 | self::KEYWORD_MAX_LENGTH && mb_strlen($value, 'UTF-8') > self::KEYWORD_MAX_LENGTH) { // strlen is faster (O(1)), so we prefer to first check using it, and then double-checking with the slower mb_strlen (O(n)) only when necessary 26 | return mb_substr($value, 0, self::KEYWORD_MAX_LENGTH - 1, 'UTF-8') . '…'; 27 | } 28 | 29 | return $value; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Helper/StackTrace.php: -------------------------------------------------------------------------------- 1 | $single_backtrace['file'], 25 | 'filename' => basename($single_backtrace['file']), 26 | 'function' => $single_backtrace['function'] ?? null, 27 | 'lineno' => $single_backtrace['line'] ?? null, 28 | 'module' => $single_backtrace['class'] ?? null, 29 | 'vars' => $single_backtrace['args'] ?? null, 30 | ]; 31 | } 32 | return $return_value; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Helper/Timer.php: -------------------------------------------------------------------------------- 1 | startedOn = $startTime; 31 | } 32 | 33 | /** 34 | * Start the Timer 35 | * 36 | * @return void 37 | * @throws AlreadyRunningException 38 | */ 39 | public function start() 40 | { 41 | if (null !== $this->startedOn) { 42 | throw new AlreadyRunningException(); 43 | } 44 | 45 | $this->startedOn = microtime(true); 46 | } 47 | 48 | /** 49 | * Stop the Timer 50 | * 51 | * @throws \PhilKra\Exception\Timer\NotStartedException 52 | * 53 | * @return void 54 | */ 55 | public function stop() 56 | { 57 | if ($this->startedOn === null) { 58 | throw new NotStartedException(); 59 | } 60 | 61 | $this->stoppedOn = microtime(true); 62 | } 63 | 64 | /** 65 | * Get the elapsed Duration of this Timer in MicroSeconds 66 | * 67 | * @throws \PhilKra\Exception\Timer\NotStoppedException 68 | * 69 | * @return float 70 | */ 71 | public function getDuration() : float 72 | { 73 | if ($this->stoppedOn === null) { 74 | throw new NotStoppedException(); 75 | } 76 | 77 | return $this->toMicro($this->stoppedOn - $this->startedOn); 78 | } 79 | 80 | /** 81 | * Get the elapsed Duration of this Timer in MilliSeconds 82 | * 83 | * @throws \PhilKra\Exception\Timer\NotStoppedException 84 | * 85 | * @return float 86 | */ 87 | public function getDurationInMilliseconds() : float 88 | { 89 | if ($this->stoppedOn === null) { 90 | throw new NotStoppedException(); 91 | } 92 | 93 | return $this->toMilli($this->stoppedOn - $this->startedOn); 94 | } 95 | 96 | /** 97 | * Get the current elapsed Interval of the Timer in MicroSeconds 98 | * 99 | * @throws \PhilKra\Exception\Timer\NotStartedException 100 | * 101 | * @return float 102 | */ 103 | public function getElapsed() : float 104 | { 105 | if ($this->startedOn === null) { 106 | throw new NotStartedException(); 107 | } 108 | 109 | return ($this->stoppedOn === null) ? 110 | $this->toMicro(microtime(true) - $this->startedOn) : 111 | $this->getDuration(); 112 | } 113 | 114 | /** 115 | * Get the current elapsed Interval of the Timer in MilliSeconds 116 | * 117 | * @throws \PhilKra\Exception\Timer\NotStartedException 118 | * 119 | * @return float 120 | */ 121 | public function getElapsedInMilliseconds() : float 122 | { 123 | if ($this->startedOn === null) { 124 | throw new NotStartedException(); 125 | } 126 | 127 | return ($this->stoppedOn === null) ? 128 | $this->toMilli(microtime(true) - $this->startedOn) : 129 | $this->getDurationInMilliseconds(); 130 | } 131 | 132 | /** 133 | * Convert the Duration from Seconds to Micro-Seconds 134 | * 135 | * @param float $num 136 | * 137 | * @return float 138 | */ 139 | private function toMicro(float $num) : float 140 | { 141 | return $num * 1000000; 142 | } 143 | 144 | /** 145 | * Convert the Duration from Seconds to Milli-Seconds 146 | * 147 | * @param float $num 148 | * 149 | * @return float 150 | */ 151 | private function toMilli(float $num) : float 152 | { 153 | return $num * 1000; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Middleware/Connector.php: -------------------------------------------------------------------------------- 1 | config = $config; 40 | $this->configureHttpClient(); 41 | } 42 | 43 | /** 44 | * Is the Payload Queue populated? 45 | * 46 | * @return bool 47 | */ 48 | public function isPayloadSet() : bool 49 | { 50 | return (empty($this->payload) === false); 51 | } 52 | 53 | /** 54 | * Create and configure the HTTP client 55 | * 56 | * @return void 57 | */ 58 | private function configureHttpClient() 59 | { 60 | $httpClientDefaults = [ 61 | 'timeout' => $this->config->get('timeout'), 62 | ]; 63 | 64 | $httpClientConfig = $this->config->get('httpClient') ?? []; 65 | 66 | $this->client = new Client(array_merge($httpClientDefaults, $httpClientConfig)); 67 | } 68 | 69 | /** 70 | * Put Events to the Payload Queue 71 | */ 72 | public function putEvent(EventBean $event) 73 | { 74 | $this->payload[] = json_encode($event); 75 | } 76 | 77 | /** 78 | * Commit the Events to the APM server 79 | * 80 | * @return bool 81 | */ 82 | public function commit() : bool 83 | { 84 | $body = ''; 85 | foreach($this->payload as $line) { 86 | $body .= $line . "\n"; 87 | } 88 | $this->payload = []; 89 | $response = $this->client->post($this->getEndpoint(), [ 90 | 'headers' => $this->getRequestHeaders(), 91 | 'body' => $body, 92 | ]); 93 | return ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300); 94 | } 95 | 96 | /** 97 | * Get the Server Informations 98 | * 99 | * @link https://www.elastic.co/guide/en/apm/server/7.3/server-info.html 100 | * 101 | * @return Response 102 | */ 103 | public function getInfo() : \GuzzleHttp\Psr7\Response 104 | { 105 | return $this->client->get( 106 | $this->config->get('serverUrl'), 107 | ['headers' => $this->getRequestHeaders(),] 108 | ); 109 | } 110 | 111 | /** 112 | * Get the Endpoint URI of the APM Server 113 | * 114 | * @param string $endpoint 115 | * 116 | * @return string 117 | */ 118 | private function getEndpoint() : string 119 | { 120 | return sprintf('%s/intake/v2/events', $this->config->get('serverUrl')); 121 | } 122 | 123 | /** 124 | * Get the Headers for the POST Request 125 | * 126 | * @return array 127 | */ 128 | private function getRequestHeaders() : array 129 | { 130 | // Default Headers Set 131 | $headers = [ 132 | 'Content-Type' => 'application/x-ndjson', 133 | 'User-Agent' => sprintf('elasticapm-php/%s', Agent::VERSION), 134 | 'Accept' => 'application/json', 135 | ]; 136 | 137 | // Add Secret Token to Header 138 | if ($this->config->get('secretToken') !== null) { 139 | $headers['Authorization'] = sprintf('Bearer %s', $this->config->get('secretToken')); 140 | } 141 | 142 | return $headers; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/Stores/Store.php: -------------------------------------------------------------------------------- 1 | store; 29 | } 30 | 31 | /** 32 | * Is the Store Empty ? 33 | * 34 | * @return bool 35 | */ 36 | public function isEmpty() : bool 37 | { 38 | return empty($this->store); 39 | } 40 | 41 | /** 42 | * Empty the Store 43 | * 44 | * @return void 45 | */ 46 | public function reset() 47 | { 48 | $this->store = []; 49 | } 50 | 51 | /** 52 | * Serialize the Events Store 53 | * 54 | * @return array 55 | */ 56 | public function jsonSerialize() : array 57 | { 58 | return $this->store; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Stores/TransactionsStore.php: -------------------------------------------------------------------------------- 1 | getTransactionName(); 27 | 28 | // Do not override the 29 | if (isset($this->store[$name]) === true) { 30 | throw new DuplicateTransactionNameException($name); 31 | } 32 | 33 | // Push to Store 34 | $this->store[$name] = $transaction; 35 | } 36 | 37 | /** 38 | * Fetch a Transaction from the Store 39 | * 40 | * @param final string $name 41 | * 42 | * @return mixed: \PhilKra\Events\Transaction | null 43 | */ 44 | public function fetch(string $name) 45 | { 46 | return $this->store[$name] ?? null; 47 | } 48 | 49 | /** 50 | * Serialize the Transactions Events Store 51 | * 52 | * @return array 53 | */ 54 | public function jsonSerialize() : array 55 | { 56 | return array_values($this->store); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Traits/Events/Stacktrace.php: -------------------------------------------------------------------------------- 1 | getDebugBacktrace($limit)); 20 | } 21 | 22 | /** 23 | * Function to convert debug_backtrace results to an array of stack frames 24 | * 25 | * @param int $limit 26 | * 27 | * @return array 28 | */ 29 | protected function getDebugBacktrace(int $limit) : array 30 | { 31 | $traces = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit); 32 | for ($it = 1; $it < count($traces); $it++) { 33 | if(isset($traces[$it]['file']) === true) { 34 | $backtrace[] = [ 35 | 'abs_path' => $traces[$it]['file'], 36 | 'filename' => basename($traces[$it]['file']), 37 | 'function' => $traces[$it]['function'] ?? null, 38 | 'lineno' => $traces[$it]['line'] ?? null, 39 | 'module' => $traces[$it]['class'] ?? null, 40 | 'vars' => $traces[$it]['args'] ?? null, 41 | ]; 42 | } 43 | } 44 | 45 | return $backtrace; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /tests/AgentTest.php: -------------------------------------------------------------------------------- 1 | 'phpunit_1', 'active' => false, ] ); 21 | 22 | // Create a Transaction, wait and Stop it 23 | $name = 'trx'; 24 | $agent->startTransaction( $name ); 25 | usleep( 10 * 1000 ); // sleep milliseconds 26 | $agent->stopTransaction( $name ); 27 | 28 | // Transaction Summary must be populated 29 | $summary = $agent->getTransaction( $name )->getSummary(); 30 | 31 | $this->assertArrayHasKey( 'duration', $summary ); 32 | $this->assertArrayHasKey( 'backtrace', $summary ); 33 | 34 | // Expect duration in milliseconds 35 | $this->assertDurationIsWithinThreshold(10, $summary['duration']); 36 | $this->assertNotEmpty( $summary['backtrace'] ); 37 | } 38 | 39 | /** 40 | * @covers \PhilKra\Agent::__construct 41 | * @covers \PhilKra\Agent::startTransaction 42 | * @covers \PhilKra\Agent::stopTransaction 43 | * @covers \PhilKra\Agent::getTransaction 44 | */ 45 | public function testStartAndStopATransactionWithExplicitStart() 46 | { 47 | $agent = new Agent( [ 'appName' => 'phpunit_1', 'active' => false, ] ); 48 | 49 | // Create a Transaction, wait and Stop it 50 | $name = 'trx'; 51 | $agent->startTransaction( $name, [], microtime(true) - 1); 52 | usleep( 500 * 1000 ); // sleep milliseconds 53 | $agent->stopTransaction( $name ); 54 | 55 | // Transaction Summary must be populated 56 | $summary = $agent->getTransaction( $name )->getSummary(); 57 | 58 | $this->assertArrayHasKey( 'duration', $summary ); 59 | $this->assertArrayHasKey( 'backtrace', $summary ); 60 | 61 | // Expect duration in milliseconds 62 | $this->assertDurationIsWithinThreshold(1500, $summary['duration'], 150); 63 | $this->assertNotEmpty( $summary['backtrace'] ); 64 | } 65 | 66 | /** 67 | * @depends testStartAndStopATransaction 68 | * 69 | * @covers \PhilKra\Agent::__construct 70 | * @covers \PhilKra\Agent::getTransaction 71 | */ 72 | public function testForceErrorOnUnknownTransaction() 73 | { 74 | $agent = new Agent( [ 'appName' => 'phpunit_x', 'active' => false, ] ); 75 | 76 | $this->expectException( \PhilKra\Exception\Transaction\UnknownTransactionException::class ); 77 | 78 | // Let it go boom! 79 | $agent->getTransaction( 'unknown' ); 80 | } 81 | 82 | /** 83 | * @depends testForceErrorOnUnknownTransaction 84 | * 85 | * @covers \PhilKra\Agent::__construct 86 | * @covers \PhilKra\Agent::stopTransaction 87 | */ 88 | public function testForceErrorOnUnstartedTransaction() 89 | { 90 | $agent = new Agent( [ 'appName' => 'phpunit_2', 'active' => false, ] ); 91 | 92 | $this->expectException( \PhilKra\Exception\Transaction\UnknownTransactionException::class ); 93 | 94 | // Stop an unstarted Transaction and let it go boom! 95 | $agent->stopTransaction( 'unknown' ); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tests/Events/TransactionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($name, $transaction->getTransactionName()); 23 | $this->assertNotNull($transaction->getId()); 24 | $this->assertNotNull($transaction->getTraceId()); 25 | $this->assertNotNull($transaction->getTimestamp()); 26 | 27 | $now = round(microtime(true) * 1000000); 28 | $this->assertGreaterThanOrEqual($transaction->getTimestamp(), $now); 29 | } 30 | 31 | /** 32 | * @depends testParentConstructor 33 | * 34 | * @covers \PhilKra\Events\EventBean::setParent 35 | * @covers \PhilKra\Events\EventBean::getTraceId 36 | * @covers \PhilKra\Events\EventBean::ensureGetTraceId 37 | */ 38 | public function testParentReference() { 39 | $parent = new Transaction('parent', []); 40 | $child = new Transaction('child', []); 41 | $child->setParent($parent); 42 | 43 | $arr = json_decode(json_encode($child), true); 44 | 45 | $this->assertEquals($arr['transaction']['id'], $child->getId()); 46 | $this->assertEquals($arr['transaction']['parent_id'], $parent->getId()); 47 | $this->assertEquals($arr['transaction']['trace_id'], $parent->getTraceId()); 48 | $this->assertEquals($child->getTraceId(), $parent->getTraceId()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Helper/ConfigTest.php: -------------------------------------------------------------------------------- 1 | $appName, 'active' => false, ] ); 21 | 22 | // Control Default Config 23 | $config = $agent->getConfig()->asArray(); 24 | 25 | $this->assertArrayHasKey( 'appName', $config ); 26 | $this->assertArrayHasKey( 'secretToken', $config ); 27 | $this->assertArrayHasKey( 'serverUrl', $config ); 28 | $this->assertArrayHasKey( 'hostname', $config ); 29 | $this->assertArrayHasKey( 'active', $config ); 30 | $this->assertArrayHasKey( 'timeout', $config ); 31 | $this->assertArrayHasKey( 'appVersion', $config ); 32 | $this->assertArrayHasKey( 'env', $config ); 33 | $this->assertArrayHasKey( 'cookies', $config ); 34 | $this->assertArrayHasKey( 'httpClient', $config ); 35 | $this->assertArrayHasKey( 'environment', $config ); 36 | $this->assertArrayHasKey( 'backtraceLimit', $config ); 37 | 38 | $this->assertEquals( $config['appName'], $appName ); 39 | $this->assertNull( $config['secretToken'] ); 40 | $this->assertEquals( $config['serverUrl'], 'http://127.0.0.1:8200' ); 41 | $this->assertEquals( $config['hostname'], gethostname() ); 42 | $this->assertFalse( $config['active'] ); 43 | $this->assertEquals( $config['timeout'], 10 ); 44 | $this->assertEquals( $config['env'], ['SERVER_SOFTWARE'] ); 45 | $this->assertEquals( $config['cookies'], [] ); 46 | $this->assertEquals( $config['httpClient'], [] ); 47 | $this->assertEquals( $config['environment'], 'development' ); 48 | $this->assertEquals( $config['backtraceLimit'], 0 ); 49 | } 50 | 51 | /** 52 | * @depends testControlDefaultConfig 53 | * 54 | * @covers \PhilKra\Helper\Config::__construct 55 | * @covers \PhilKra\Agent::getConfig 56 | * @covers \PhilKra\Helper\Config::getDefaultConfig 57 | * @covers \PhilKra\Helper\Config::asArray 58 | */ 59 | public function testControlInjectedConfig() { 60 | $init = [ 61 | 'appName' => sprintf( 'app_name_%d', rand( 10, 99 ) ), 62 | 'secretToken' => hash( 'tiger128,3', time() ), 63 | 'serverUrl' => sprintf( 'https://node%d.domain.tld:%d', rand( 10, 99 ), rand( 1000, 9999 ) ), 64 | 'appVersion' => sprintf( '%d.%d.42', rand( 0, 3 ), rand( 0, 10 ) ), 65 | 'frameworkName' => uniqid(), 66 | 'timeout' => rand( 10, 20 ), 67 | 'hostname' => sprintf( 'host_%d', rand( 0, 9 ) ), 68 | 'active' => false, 69 | ]; 70 | 71 | $agent = new Agent( $init ); 72 | 73 | // Control Default Config 74 | $config = $agent->getConfig()->asArray(); 75 | foreach( $init as $key => $value ) { 76 | $this->assertEquals( $config[$key], $init[$key], 'key: ' . $key ); 77 | } 78 | } 79 | 80 | /** 81 | * @depends testControlInjectedConfig 82 | * 83 | * @covers \PhilKra\Helper\Config::__construct 84 | * @covers \PhilKra\Agent::getConfig 85 | * @covers \PhilKra\Helper\Config::getDefaultConfig 86 | * @covers \PhilKra\Helper\Config::get 87 | */ 88 | public function testGetConfig() { 89 | $init = [ 90 | 'appName' => sprintf( 'app_name_%d', rand( 10, 99 ) ), 91 | 'active' => false, 92 | ]; 93 | 94 | $agent = new Agent( $init ); 95 | $this->assertEquals( $agent->getConfig()->get( 'appName' ), $init['appName'] ); 96 | } 97 | 98 | /** 99 | * @depends testControlDefaultConfig 100 | * 101 | * @covers \PhilKra\Helper\Config::__construct 102 | * @covers \PhilKra\Agent::getConfig 103 | * @covers \PhilKra\Helper\Config::getDefaultConfig 104 | * @covers \PhilKra\Helper\Config::asArray 105 | */ 106 | public function testTrimElasticServerUrl() { 107 | $init = [ 108 | 'serverUrl' => 'http://foo.bar/', 109 | 'appName' => sprintf( 'app_name_%d', rand( 10, 99 ) ), 110 | 'active' => false, 111 | ]; 112 | 113 | $agent = new Agent( $init ); 114 | $config = $agent->getConfig()->asArray(); 115 | foreach( $init as $key => $value ) { 116 | if ('serverUrl' === $key) { 117 | $this->assertEquals('http://foo.bar', $config[$key]); 118 | } else { 119 | $this->assertEquals( $config[$key], $init[$key], 'key: ' . $key ); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Helper/DistributedTracingTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("0bfda6be83a31fb66a455cbb74a70344", $traceParent->getTraceId()); 24 | $this->assertEquals("6b84fae6bd7064af", $traceParent->getParentId()); 25 | $this->assertEquals("01", $traceParent->getTraceFlags()); 26 | $this->assertEquals($header, $traceParent->__toString()); 27 | } 28 | 29 | /** 30 | * @covers \PhilKra\Helper\DistributedTracing::isValidHeader 31 | * @covers \PhilKra\Helper\DistributedTracing::createFromHeader 32 | */ 33 | public function testCannotCreateFromInvalidHeader() { 34 | $invalidHeaders = [ 35 | "", 36 | "118c6c15301b9b3b3:56e66177e6e55a91:18c6c15301b9b3b3:1" 37 | ]; 38 | 39 | $this->expectException( \PhilKra\Exception\InvalidTraceContextHeaderException::class ); 40 | 41 | foreach ($invalidHeaders as $header) { 42 | DistributedTracing::createFromHeader($header); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /tests/Helper/EncodingTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( $input, Encoding::keywordField($input) ); 20 | 21 | } 22 | 23 | /** 24 | * @covers \PhilKra\Helper\Encoding::keywordField 25 | */ 26 | public function testLongInput() { 27 | 28 | $input = str_repeat("abc123", 200); 29 | $output = str_repeat("abc123", 170) . 'abc' . '…'; 30 | 31 | $this->assertEquals( $output, Encoding::keywordField($input) ); 32 | 33 | } 34 | 35 | /** 36 | * @covers \PhilKra\Helper\Encoding::keywordField 37 | */ 38 | public function testLongMultibyteInput() { 39 | 40 | $input = str_repeat("中国日本韓国合衆国", 200); 41 | $output = str_repeat("中国日本韓国合衆国", 113) . '中国日本韓国' . '…'; 42 | 43 | $this->assertEquals( $output, Encoding::keywordField($input) ); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/Helper/TimerTest.php: -------------------------------------------------------------------------------- 1 | start(); 24 | usleep( $duration ); 25 | $timer->stop(); 26 | 27 | $this->assertGreaterThanOrEqual( $duration, $timer->getDuration() ); 28 | } 29 | 30 | /** 31 | * @covers \PhilKra\Helper\Timer::start 32 | * @covers \PhilKra\Helper\Timer::stop 33 | * @covers \PhilKra\Helper\Timer::getDuration 34 | * @covers \PhilKra\Helper\Timer::toMicro 35 | */ 36 | public function testCanCalculateDurationInMilliseconds() { 37 | $timer = new Timer(); 38 | $duration = rand( 25, 100 ); // duration in milliseconds 39 | 40 | $timer->start(); 41 | usleep( $duration * 1000 ); // sleep microseconds 42 | $timer->stop(); 43 | 44 | $this->assertDurationIsWithinThreshold($duration, $timer->getDurationInMilliseconds()); 45 | } 46 | 47 | /** 48 | * @depends testCanBeStartedAndStoppedWithDuration 49 | * 50 | * @covers \PhilKra\Helper\Timer::start 51 | * @covers \PhilKra\Helper\Timer::stop 52 | * @covers \PhilKra\Helper\Timer::getDuration 53 | * @covers \PhilKra\Helper\Timer::getElapsed 54 | * @covers \PhilKra\Helper\Timer::toMicro 55 | */ 56 | public function testGetElapsedDurationWithoutError() { 57 | $timer = new Timer(); 58 | 59 | $timer->start(); 60 | usleep( 10 ); 61 | $elapsed = $timer->getElapsed(); 62 | $timer->stop(); 63 | 64 | $this->assertGreaterThanOrEqual( $elapsed, $timer->getDuration() ); 65 | $this->assertEquals( $timer->getElapsed(), $timer->getDuration() ); 66 | } 67 | 68 | /** 69 | * @depends testCanBeStartedAndStoppedWithDuration 70 | * 71 | * @covers \PhilKra\Helper\Timer::start 72 | * @covers \PhilKra\Helper\Timer::getDuration 73 | */ 74 | public function testCanBeStartedWithForcingDurationException() { 75 | $timer = new Timer(); 76 | $timer->start(); 77 | 78 | $this->expectException( \PhilKra\Exception\Timer\NotStoppedException::class ); 79 | 80 | $timer->getDuration(); 81 | } 82 | 83 | /** 84 | * @depends testCanBeStartedWithForcingDurationException 85 | * 86 | * @covers \PhilKra\Helper\Timer::stop 87 | */ 88 | public function testCannotBeStoppedWithoutStart() { 89 | $timer = new Timer(); 90 | 91 | $this->expectException( \PhilKra\Exception\Timer\NotStartedException::class ); 92 | 93 | $timer->stop(); 94 | } 95 | 96 | /** 97 | * @covers \PhilKra\Helper\Timer::start 98 | * @covers \PhilKra\Helper\Timer::getDurationInMilliseconds 99 | */ 100 | public function testCanBeStartedWithExplicitStartTime() { 101 | $timer = new Timer(microtime(true) - .5); // Start timer 500 milliseconds ago 102 | 103 | usleep(500 * 1000); // Sleep for 500 milliseconds 104 | 105 | $timer->stop(); 106 | 107 | $duration = $timer->getDurationInMilliseconds(); 108 | 109 | // Duration should be more than 1000 milliseconds 110 | // sum of initial offset and sleep 111 | $this->assertGreaterThanOrEqual(1000, $duration); 112 | } 113 | 114 | /** 115 | * @covers \PhilKra\Helper\Timer::start 116 | */ 117 | public function testCannotBeStartedIfAlreadyRunning() { 118 | $timer = new Timer(microtime(true)); 119 | 120 | $this->expectException(AlreadyRunningException::class); 121 | $timer->start(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/PHPUnitUtils.php: -------------------------------------------------------------------------------- 1 | getMethod($name); 15 | $method->setAccessible(true); 16 | return $method->invokeArgs($obj, $args); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /tests/Stores/TransactionsStoreTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( $store->isEmpty() ); 24 | 25 | // Store the Transaction and fetch it then 26 | $store->register( $trx ); 27 | $proof = $store->fetch( $name ); 28 | 29 | // We should get the Same! 30 | $this->assertEquals( $trx, $proof ); 31 | $this->assertNotNull( $proof ); 32 | 33 | // Must not be Empty 34 | $this->assertFalse( $store->isEmpty() ); 35 | } 36 | 37 | /** 38 | * @depends testTransactionRegistrationAndFetch 39 | * 40 | * @covers \PhilKra\Stores\TransactionsStore::register 41 | */ 42 | public function testDuplicateTransactionRegistration() { 43 | $store = new TransactionsStore(); 44 | $name = 'test'; 45 | $trx = new Transaction( $name, [] ); 46 | 47 | $this->expectException( \PhilKra\Exception\Transaction\DuplicateTransactionNameException::class ); 48 | 49 | // Store the Transaction again to force an Exception 50 | $store->register( $trx ); 51 | $store->register( $trx ); 52 | } 53 | 54 | /** 55 | * @depends testTransactionRegistrationAndFetch 56 | * 57 | * @covers \PhilKra\Stores\TransactionsStore::get 58 | */ 59 | public function testFetchUnknownTransaction() { 60 | $store = new TransactionsStore(); 61 | $this->assertNull( $store->fetch( 'unknown' ) ); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertGreaterThanOrEqual( $expectedMilliseconds, $timedDuration ); 18 | 19 | $overhead = ($timedDuration - $expectedMilliseconds); 20 | $this->assertLessThanOrEqual( $maxOverhead, $overhead ); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /tests/Traits/Events/StacktraceTest.php: -------------------------------------------------------------------------------- 1 | stacktraceMock = $this->getMockForTrait(Stacktrace::class); 20 | } 21 | 22 | protected function tearDown() 23 | { 24 | $this->stacktraceMock = null; 25 | } 26 | 27 | /** 28 | * @covers \PhilKra\Traits\Events\Stacktrace::getDebugBacktrace 29 | */ 30 | public function testEntry() 31 | { 32 | $n = rand(4, 7); 33 | $result = PHPUnitUtils::callMethod($this->stacktraceMock, 'getDebugBacktrace', [$n]); 34 | 35 | // Ensure the first elem is not present (self) 36 | $this->assertEquals(count($result), ($n - 1)); 37 | 38 | $this->assertArrayHasKey('abs_path', $result[0]); 39 | $this->assertArrayHasKey('filename', $result[0]); 40 | $this->assertArrayHasKey('function', $result[0]); 41 | $this->assertArrayHasKey('lineno', $result[0]); 42 | $this->assertArrayHasKey('module', $result[0]); 43 | $this->assertArrayHasKey('vars', $result[0]); 44 | $this->assertArrayHasKey(1, $result[0]['vars']); 45 | 46 | $this->assertStringEndsWith('tests/PHPUnitUtils.php', $result[0]['abs_path']); 47 | $this->assertEquals('PHPUnitUtils.php', $result[0]['filename']); 48 | $this->assertEquals('invokeArgs', $result[0]['function']); 49 | $this->assertEquals(16, $result[0]['lineno']); 50 | $this->assertEquals('ReflectionMethod', $result[0]['module']); 51 | $this->assertEquals($n, $result[0]['vars'][1][0]); 52 | 53 | $this->assertStringEndsWith('tests/Traits/Events/StacktraceTest.php', $result[1]['abs_path']); 54 | $this->assertEquals('StacktraceTest.php', $result[1]['filename']); 55 | $this->assertEquals('callMethod', $result[1]['function']); 56 | $this->assertEquals(33, $result[1]['lineno']); 57 | $this->assertEquals('PhilKra\Tests\PHPUnitUtils', $result[1]['module']); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |