Completed
Push — master ( a06f3f...4cb0fb )
by Shagiakhmetov
06:20
created

LiveProfiler::getApiKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
/**
4
 * @maintainer Timur Shagiakhmetov <[email protected]>
5
 */
6
7
namespace Badoo\LiveProfiler;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\DBALException;
11
use Psr\Log\LoggerInterface;
12
13
class LiveProfiler
14
{
15
    CONST MODE_DB = 'db';
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected const, but found CONST.
Loading history...
16
    CONST MODE_FILES = 'files';
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected const, but found CONST.
Loading history...
17
    CONST MODE_API = 'api';
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected const, but found CONST.
Loading history...
18
19
    /** @var LiveProfiler */
20
    protected static $instance;
21
    /** @var string */
22
    protected $mode = self::MODE_DB;
23
    /** @var string */
24
    protected $path = '';
25
    /** @var string */
26
    protected $api_key = '';
27
    /** @var string */
28
    protected $url = 'http://liveprof.org/api';
29
    /** @var Connection */
30
    protected $Conn;
31
    /** @var LoggerInterface */
32
    protected $Logger;
33
    /** @var DataPackerInterface */
34
    protected $DataPacker;
35
    /** @var string */
36
    protected $connection_string;
37
    /** @var string */
38
    protected $app;
39
    /** @var string */
40
    protected $label;
41
    /** @var string */
42
    protected $datetime;
43
    /** @var int */
44
    protected $divider = 1000;
45
    /** @var int */
46
    protected $total_divider = 10000;
47
    /** @var callable callback to start profiling */
48
    protected $start_callback;
49
    /** @var callable callback to end profiling */
50
    protected $end_callback;
51
    /** @var bool */
52
    protected $is_enabled = false;
53
    /** @var array */
54
    protected $last_profile_data = [];
55
56
    /**
57
     * LiveProfiler constructor.
58
     * @param string $connection_string_or_path
59
     * @param string $mode
60
     */
61 17
    public function __construct($connection_string_or_path = '', $mode = self::MODE_DB)
62
    {
63 17
        $this->mode = $mode;
64
65 17
        $this->app = 'Default';
66 17
        $this->label = $this->getAutoLabel();
67 17
        $this->datetime = date('Y-m-d H:i:s');
68
69 17
        $this->detectProfiler();
70 17
        $this->Logger = new Logger();
71 17
        $this->DataPacker = new DataPacker();
72
73 17
        if ($mode === self::MODE_DB) {
74 15
            $this->connection_string = $connection_string_or_path ?: getenv('LIVE_PROFILER_CONNECTION_URL');
75 2
        } elseif ($mode === self::MODE_API) {
76 1
            if ($connection_string_or_path) {
77
                $this->url = $connection_string_or_path;
78 1
            } elseif (getenv('LIVE_PROFILER_API_URL')) {
79 1
                $this->url = getenv('LIVE_PROFILER_API_URL');
80
            }
81
        } else {
82 1
            $this->setPath($connection_string_or_path ?: getenv('LIVE_PROFILER_PATH'));
83
        }
84 17
    }
85
86 1
    public static function getInstance($connection_string = '', $mode = self::MODE_DB)
87
    {
88 1
        if (self::$instance === null) {
89 1
            self::$instance = new static($connection_string, $mode);
90
        }
91
92 1
        return self::$instance;
93
    }
94
95 7
    public function start()
96
    {
97 7
        if ($this->is_enabled) {
98 1
            return true;
99
        }
100
101 6
        if (null === $this->start_callback) {
102 1
            return true;
103
        }
104
105 5
        if ($this->needToStart($this->divider)) {
106 4
            $this->is_enabled = true;
107 1
        } elseif ($this->needToStart($this->total_divider)) {
108 1
            $this->is_enabled = true;
109 1
            $this->label = 'All';
110
        }
111
112 5
        if ($this->is_enabled) {
113 5
            register_shutdown_function([$this, 'end']);
114 5
            call_user_func($this->start_callback);
115
        }
116
117 5
        return true;
118
    }
119
120
    /**
121
     * @return bool
122
     */
123 6
    public function end()
124
    {
125 6
        if (!$this->is_enabled) {
126 1
            return true;
127
        }
128
129 5
        $this->is_enabled = false;
130
131 5
        if (null === $this->end_callback) {
132 1
            return true;
133
        }
134
135 4
        $data = call_user_func($this->end_callback);
136 4
        if (!is_array($data)) {
137 1
            $this->Logger->warning('Invalid profiler data: ' . var_export($data, true));
138 1
            return false;
139
        }
140
141 3
        $this->last_profile_data = $data;
142 3
        $result = $this->save($this->app, $this->label, $this->datetime, $data);
143
144 3
        if (!$result) {
145 2
            $this->Logger->warning('Can\'t insert profile data');
146
        }
147
148 3
        return $result;
149
    }
150
151 18
    public function detectProfiler()
152
    {
153 18
        if (function_exists('xhprof_enable')) {
154
            return $this->useXhprof();
155
        }
156
157 18
        if (function_exists('tideways_xhprof_enable')) {
158
            return $this->useTidyWays();
159
        }
160
161 18
        if (function_exists('uprofiler_enable')) {
162
            return $this->useUprofiler();
163
        }
164
165 18
        return false;
166
    }
167
168 2
    public function useXhprof()
169
    {
170 2
        if ($this->is_enabled) {
171 1
            $this->Logger->warning('can\'t change profiler after profiling started');
172 1
            return false;
173
        }
174
175
        $this->start_callback = function () {
176
            xhprof_enable(XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU);
177
        };
178
179
        $this->end_callback = function () {
180
            return xhprof_disable();
181
        };
182
183 1
        return true;
184
    }
185
186 2
    public function useXhprofSample()
187
    {
188 2
        if ($this->is_enabled) {
189 1
            $this->Logger->warning('can\'t change profiler after profiling started');
190 1
            return false;
191
        }
192
193 1
        if (!ini_get('xhprof.sampling_interval')) {
194 1
            ini_set('xhprof.sampling_interval', 10000);
195
        }
196
197 1
        if (!ini_get('xhprof.sampling_depth')) {
198 1
            ini_set('xhprof.sampling_depth', 200);
199
        }
200
201
        $this->start_callback = function () {
202
            define('XHPROF_SAMPLING_BEGIN', microtime(true));
203
            xhprof_sample_enable();
204
        };
205
206
        $this->end_callback = function () {
207
            return $this->convertSampleDataToCommonFormat(xhprof_sample_disable());
208
        };
209
210 1
        return true;
211
    }
212
213 1
    protected function convertSampleDataToCommonFormat(array $sampling_data)
214
    {
215 1
        $result_data = [];
216 1
        $prev_time = XHPROF_SAMPLING_BEGIN;
217 1
        foreach ($sampling_data as $time => $callstack) {
218 1
            $wt = (int)(($time - $prev_time) * 1000000);
219 1
            $functions = explode('==>', $callstack);
220 1
            $prev_i = 0;
221 1
            $main_key = $functions[$prev_i];
222 1
            if (!isset($result_data[$main_key])) {
223 1
                $result_data[$main_key] = [
224
                    'ct' => 0,
225
                    'wt' => 0,
226
                ];
227
            }
228 1
            $result_data[$main_key]['ct'] ++;
229 1
            $result_data[$main_key]['wt'] += $wt;
230
231 1
            $func_cnt = count($functions);
232 1
            for ($i = 1; $i < $func_cnt; $i++) {
233 1
                $key = $functions[$prev_i] . '==>' . $functions[$i];
234
235 1
                if (!isset($result_data[$key])) {
236 1
                    $result_data[$key] = [
237
                        'ct' => 0,
238
                        'wt' => 0,
239
                    ];
240
                }
241
242 1
                $result_data[$key]['wt'] += $wt;
243 1
                $result_data[$key]['ct']++;
244
245 1
                $prev_i = $i;
246
            }
247
248 1
            $prev_time = $time;
249
        }
250
251 1
        return $result_data;
252
    }
253
254 2
    public function useTidyWays()
255
    {
256 2
        if ($this->is_enabled) {
257 1
            $this->Logger->warning('can\'t change profiler after profiling started');
258 1
            return false;
259
        }
260
261
        $this->start_callback = function () {
262
            tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
263
        };
264
265
        $this->end_callback = function () {
266
            return tideways_xhprof_disable();
267
        };
268
269 1
        return true;
270
    }
271
272 2
    public function useUprofiler()
273
    {
274 2
        if ($this->is_enabled) {
275 1
            $this->Logger->warning('can\'t change profiler after profiling started');
276 1
            return false;
277
        }
278
279
        $this->start_callback = function () {
280
            uprofiler_enable(UPROFILER_FLAGS_CPU | UPROFILER_FLAGS_MEMORY);
281
        };
282
283
        $this->end_callback = function () {
284
            return uprofiler_disable();
285
        };
286
287 1
        return true;
288
    }
289
290
    /**
291
     * @return bool
292
     */
293 1
    public function reset()
294
    {
295 1
        if ($this->is_enabled) {
296 1
            call_user_func($this->end_callback);
297 1
            $this->is_enabled = false;
298
        }
299
300 1
        return true;
301
    }
302
303
    /**
304
     * @param string $mode
305
     * @return $this
306
     */
307 1
    public function setMode($mode)
308
    {
309 1
        $this->mode = $mode;
310 1
        return $this;
311
    }
312
313
    /**
314
     * @return string
315
     */
316 1
    public function getMode()
317
    {
318 1
        return $this->mode;
319
    }
320
321
    /**
322
     * @param string $path
323
     * @return $this
324
     */
325 2
    public function setPath($path)
326
    {
327 2
        if (!is_dir($path)) {
328 1
            $this->Logger->error('Directory ' . $path . ' does not exists');
329
        }
330
331 2
        $this->path = $path;
332 2
        return $this;
333
    }
334
335
    /**
336
     * @return string
337
     */
338 1
    public function getPath()
339
    {
340 1
        return $this->path;
341
    }
342
343
    /**
344
     * @param string $api_key
345
     * @return $this
346
     */
347 2
    public function setApiKey($api_key)
348
    {
349 2
        $this->api_key = $api_key;
350 2
        return $this;
351
    }
352
353
    /**
354
     * @return string
355
     */
356 1
    public function getApiKey()
357
    {
358 1
        return $this->api_key;
359
    }
360
361
    /**
362
     * @param string $app
363
     * @return $this
364
     */
365 1
    public function setApp($app)
366
    {
367 1
        $this->app = $app;
368 1
        return $this;
369
    }
370
371
    /**
372
     * @return string
373
     */
374 1
    public function getApp()
375
    {
376 1
        return $this->app;
377
    }
378
379
    /**
380
     * @param string $label
381
     * @return $this
382
     */
383 1
    public function setLabel($label)
384
    {
385 1
        $this->label = $label;
386 1
        return $this;
387
    }
388
389
    /**
390
     * @return string
391
     */
392 1
    public function getLabel()
393
    {
394 1
        return $this->label;
395
    }
396
397
    /**
398
     * @param string $datetime
399
     * @return $this
400
     */
401 1
    public function setDateTime($datetime)
402
    {
403 1
        $this->datetime = $datetime;
404 1
        return $this;
405
    }
406
407
    /**
408
     * @return string
409
     */
410 1
    public function getDateTime()
411
    {
412 1
        return $this->datetime;
413
    }
414
415
    /**
416
     * @param int $divider
417
     * @return $this
418
     */
419 5
    public function setDivider($divider)
420
    {
421 5
        $this->divider = $divider;
422 5
        return $this;
423
    }
424
425
    /**
426
     * @param int $total_divider
427
     * @return $this
428
     */
429 2
    public function setTotalDivider($total_divider)
430
    {
431 2
        $this->total_divider = $total_divider;
432 2
        return $this;
433
    }
434
435
    /**
436
     * @param \Closure $start_callback
437
     * @return $this
438
     */
439 5
    public function setStartCallback(\Closure $start_callback)
440
    {
441 5
        $this->start_callback = $start_callback;
442 5
        return $this;
443
    }
444
445
    /**
446
     * @param \Closure $end_callback
447
     * @return $this
448
     */
449 5
    public function setEndCallback(\Closure $end_callback)
450
    {
451 5
        $this->end_callback = $end_callback;
452 5
        return $this;
453
    }
454
455
    /**
456
     * @param LoggerInterface $Logger
457
     * @return $this
458
     */
459 9
    public function setLogger(LoggerInterface $Logger)
460
    {
461 9
        $this->Logger = $Logger;
462 9
        return $this;
463
    }
464
465
    /**
466
     * @param DataPackerInterface $DataPacker
467
     * @return $this
468
     */
469 2
    public function setDataPacker($DataPacker)
470
    {
471 2
        $this->DataPacker = $DataPacker;
472 2
        return $this;
473
    }
474
475
    /**
476
     * @return array
477
     */
478 1
    public function getLastProfileData()
479
    {
480 1
        return $this->last_profile_data;
481
    }
482
483
    /**
484
     * @return Connection
485
     * @throws DBALException
486
     */
487 3
    protected function getConnection()
488
    {
489 3
        if (null === $this->Conn) {
490 3
            $config = new \Doctrine\DBAL\Configuration();
491 3
            $connectionParams = ['url' => $this->connection_string];
492 3
            $this->Conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
493
        }
494
495 3
        return $this->Conn;
496
    }
497
498
    /**
499
     * @param Connection $Conn
500
     * @return $this
501
     */
502 1
    public function setConnection(Connection $Conn)
503
    {
504 1
        $this->Conn = $Conn;
505 1
        return $this;
506
    }
507
508
    /**
509
     * @param string $connection_string
510
     * @return $this
511
     */
512 1
    public function setConnectionString($connection_string)
513
    {
514 1
        $this->connection_string = $connection_string;
515 1
        return $this;
516
    }
517
518
    /**
519
     * @param string $app
520
     * @param string $label
521
     * @param string $datetime
522
     * @param array $data
523
     * @return bool
524
     */
525 3
    protected function save($app, $label, $datetime, $data)
526
    {
527 3
        if ($this->mode === self::MODE_DB) {
528 1
            return $this->saveToDB($app, $label, $datetime, $data);
529
        }
530
531 2
        if ($this->mode === self::MODE_API) {
532 1
            return $this->sendToAPI($app, $label, $datetime, $data);
533
        }
534
535 1
        return $this->saveToFile($app, $label, $datetime, $data);
536
    }
537
538
    /**
539
     * @param string $app
540
     * @param string $label
541
     * @param string $datetime
542
     * @param array $data
543
     * @return bool
544
     */
545 1
    protected function sendToAPI($app, $label, $datetime, $data)
546
    {
547 1
        $data = $this->DataPacker->pack($data);
548 1
        $api_key = $this->api_key;
549 1
        $curl_handle = curl_init();
550 1
        curl_setopt($curl_handle,CURLOPT_URL,$this->url);
551 1
        curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER, true);
552 1
        curl_setopt($curl_handle, CURLOPT_POST, 1);
553 1
        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, http_build_query(compact('api_key', 'app', 'label', 'datetime', 'data')));
554 1
        curl_exec($curl_handle);
555 1
        $http_code = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE);
556 1
        curl_close($curl_handle);
557
558 1
        return $http_code === 200;
559
    }
560
561
    /**
562
     * @param string $app
563
     * @param string $label
564
     * @param string $datetime
565
     * @param array $data
566
     * @return bool
567
     */
568 1
    protected function saveToDB($app, $label, $datetime, $data)
569
    {
570 1
        $packed_data = $this->DataPacker->pack($data);
571
572
        try {
573 1
            return (bool)$this->getConnection()->insert(
574 1
                'details',
575
                [
576 1
                    'app' => $app,
577 1
                    'label' => $label,
578 1
                    'perfdata' => $packed_data,
579 1
                    'timestamp' => $datetime
580
                ]
581
            );
582
        } catch (DBALException $Ex) {
583
            $this->Logger->error('Error in insertion profile data: ' . $Ex->getMessage());
584
            return false;
585
        }
586
    }
587
588
    /**
589
     * @param string $app
590
     * @param string $label
591
     * @param string $datetime
592
     * @param array $data
593
     * @return bool
594
     */
595 1
    private function saveToFile($app, $label, $datetime, $data)
596
    {
597 1
        $path = sprintf('%s/%s/%s', $this->path, $app, base64_encode($label));
598
599 1
        if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
600
            $this->Logger->error('Directory "'. $path .'" was not created');
601
            return false;
602
        }
603
604 1
        $filename = sprintf('%s/%s.json', $path, strtotime($datetime));
605 1
        $packed_data = $this->DataPacker->pack($data);
606 1
        return (bool)file_put_contents($filename, $packed_data);
607
    }
608
609
    /**
610
     * @throws DBALException
611
     */
612 3
    public function createTable()
613
    {
614 3
        $driver_name = $this->getConnection()->getDriver()->getName();
615 3
        $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql';
616 3
        if (!file_exists($sql_path)) {
617 1
            $this->Logger->error('Invalid sql path:' . $sql_path);
618 1
            return false;
619
        }
620
621 2
        $sql = file_get_contents($sql_path);
622
623 2
        $this->getConnection()->exec($sql);
624 2
        return true;
625
    }
626
627
    /**
628
     * @return string
629
     */
630 17
    protected function getAutoLabel()
631
    {
632 17
        if (!empty($_SERVER['REQUEST_URI'])) {
633 16
            $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
634 16
            return $label ?: $_SERVER['REQUEST_URI'];
635
        }
636
637 1
        return $_SERVER['SCRIPT_NAME'];
638
    }
639
640
    /**
641
     * @param int $divider
642
     * @return bool
643
     */
644 5
    protected function needToStart($divider)
645
    {
646 5
        return mt_rand(1, $divider) === 1;
647
    }
648
}
649