├── .gitattributes ├── Clockwork ├── Web │ ├── public │ │ ├── img │ │ │ ├── icons │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── apple-touch-icon-60x60.png │ │ │ │ ├── apple-touch-icon-76x76.png │ │ │ │ ├── apple-touch-icon-120x120.png │ │ │ │ ├── apple-touch-icon-152x152.png │ │ │ │ └── apple-touch-icon-180x180.png │ │ │ ├── appearance-auto-icon.png │ │ │ ├── appearance-dark-icon.png │ │ │ ├── appearance-light-icon.png │ │ │ └── whats-new │ │ │ │ ├── 5.0 │ │ │ │ ├── toolbar.png │ │ │ │ ├── models-tab.png │ │ │ │ ├── timeline.png │ │ │ │ ├── clockwork-5.png │ │ │ │ ├── client-metrics.png │ │ │ │ └── notifications-tab.png │ │ │ │ └── 5.1 │ │ │ │ └── database-queries.png │ │ ├── manifest.json │ │ └── index.html │ └── Web.php ├── Support │ ├── Symfony │ │ ├── Resources │ │ │ └── config │ │ │ │ ├── routing │ │ │ │ └── clockwork.php │ │ │ │ └── clockwork.php │ │ ├── ClockworkBundle.php │ │ ├── ClockworkExtension.php │ │ ├── ClockworkConfiguration.php │ │ ├── ClockworkFactory.php │ │ ├── ClockworkListener.php │ │ ├── ClockworkLoader.php │ │ ├── ClockworkController.php │ │ └── ClockworkSupport.php │ ├── Laravel │ │ ├── Facade.php │ │ ├── helpers.php │ │ ├── Eloquent │ │ │ ├── ResolveModelScope.php │ │ │ └── ResolveModelLegacyScope.php │ │ ├── ClockworkMiddleware.php │ │ ├── ClockworkCleanCommand.php │ │ ├── Console │ │ │ ├── CapturingOldFormatter.php │ │ │ ├── CapturingFormatter.php │ │ │ └── CapturingLegacyFormatter.php │ │ ├── ClockworkController.php │ │ └── Tests │ │ │ ├── UsesClockwork.php │ │ │ └── ClockworkExtension.php │ ├── Vanilla │ │ ├── helpers.php │ │ ├── iframe.html.php │ │ └── ClockworkMiddleware.php │ ├── Monolog │ │ ├── Monolog │ │ │ └── ClockworkHandler.php │ │ ├── Monolog2 │ │ │ └── ClockworkHandler.php │ │ └── Handler │ │ │ └── ClockworkHandler.php │ ├── Slim │ │ ├── Old │ │ │ ├── ClockworkLogWriter.php │ │ │ └── ClockworkMiddleware.php │ │ ├── Legacy │ │ │ └── ClockworkMiddleware.php │ │ └── ClockworkMiddleware.php │ ├── Lumen │ │ ├── ClockworkMiddleware.php │ │ ├── ClockworkSupport.php │ │ ├── Controller.php │ │ └── ClockworkServiceProvider.php │ ├── Twig │ │ └── ProfilerClockworkDumper.php │ └── Swift │ │ └── SwiftPluginClockworkTimeline.php ├── Request │ ├── RequestType.php │ ├── LogLevel.php │ ├── IncomingRequest.php │ ├── UserData.php │ ├── UserDataItem.php │ ├── ShouldRecord.php │ ├── Timeline │ │ ├── Timeline.php │ │ └── Event.php │ ├── ShouldCollect.php │ └── Log.php ├── Storage │ ├── Storage.php │ ├── StorageInterface.php │ ├── SymfonyStorage.php │ ├── Search.php │ └── SqlSearch.php ├── Authentication │ ├── AuthenticatorInterface.php │ ├── NullAuthenticator.php │ └── SimpleAuthenticator.php ├── DataSource │ ├── DoctrineDataSource.php │ ├── DataSourceInterface.php │ ├── XdebugDataSource.php │ ├── MonologDataSource.php │ ├── SwiftDataSource.php │ ├── TwigDataSource.php │ ├── DataSource.php │ ├── LaravelViewsDataSource.php │ ├── Concerns │ │ └── EloquentDetectDuplicateQueries.php │ ├── LaravelQueueDataSource.php │ ├── LaravelRedisDataSource.php │ ├── PsrMessageDataSource.php │ ├── SlimDataSource.php │ ├── LaravelEventsDataSource.php │ ├── LaravelCacheDataSource.php │ ├── PhpDataSource.php │ ├── LaravelHttpClientDataSource.php │ ├── DBALDataSource.php │ ├── GuzzleDataSource.php │ ├── LumenDataSource.php │ └── LaravelDataSource.php ├── Helpers │ ├── StackFrame.php │ ├── Concerns │ │ └── ResolvesViewName.php │ ├── ServerTiming.php │ ├── StackTrace.php │ ├── StackFilter.php │ └── Serializer.php └── Clockwork.php ├── .editorconfig ├── LICENSE ├── composer.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/ export-ignore -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/appearance-auto-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-auto-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/appearance-dark-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-dark-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/appearance-light-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-light-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/toolbar.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/models-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/models-tab.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/timeline.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | 8 | [*.php] 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/client-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/client-metrics.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.1/database-queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.1/database-queries.png -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/Resources/config/routing/clockwork.php: -------------------------------------------------------------------------------- 1 | import('.', 'clockwork'); 7 | }; 8 | -------------------------------------------------------------------------------- /Clockwork/Request/RequestType.php: -------------------------------------------------------------------------------- 1 | getConnection()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Clockwork/Request/LogLevel.php: -------------------------------------------------------------------------------- 1 | debug($argument); 13 | } 14 | 15 | return reset($arguments); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Clockwork/Web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Clockwork 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Clockwork/Support/Vanilla/helpers.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 16 | } 17 | 18 | public function apply(Builder $builder, Model $model) 19 | { 20 | $this->dataSource->nextQueryModel = get_class($model); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Clockwork/Support/Monolog/Monolog/ClockworkHandler.php: -------------------------------------------------------------------------------- 1 | clockworkLog = $clockworkLog; 18 | } 19 | 20 | protected function write(array $record) 21 | { 22 | $this->clockworkLog->log($record['level'], $record['message']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Clockwork/Support/Monolog/Monolog2/ClockworkHandler.php: -------------------------------------------------------------------------------- 1 | clockworkLog = $clockworkLog; 18 | } 19 | 20 | protected function write(array $record): void 21 | { 22 | $this->clockworkLog->log($record['level'], $record['message']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Eloquent/ResolveModelLegacyScope.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 16 | } 17 | 18 | public function apply(Builder $builder, Model $model) 19 | { 20 | $this->dataSource->nextQueryModel = get_class($model); 21 | } 22 | 23 | public function remove(Builder $builder, Model $model) 24 | { 25 | // nothing to do here 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Clockwork/Support/Monolog/Handler/ClockworkHandler.php: -------------------------------------------------------------------------------- 1 | clockworkLog = $clockworkLog; 19 | } 20 | 21 | protected function write(array $record) 22 | { 23 | $this->clockworkLog->log($record['level'], $record['message']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Clockwork/Authentication/SimpleAuthenticator.php: -------------------------------------------------------------------------------- 1 | password = $password; 10 | } 11 | 12 | public function attempt(array $credentials) 13 | { 14 | if (! isset($credentials['password'])) { 15 | return false; 16 | } 17 | 18 | if (! hash_equals($credentials['password'], $this->password)) { 19 | return false; 20 | } 21 | 22 | return password_hash($this->password, \PASSWORD_DEFAULT); 23 | } 24 | 25 | public function check($token) 26 | { 27 | return password_verify($this->password, $token); 28 | } 29 | 30 | public function requires() 31 | { 32 | return [ AuthenticatorInterface::REQUIRES_PASSWORD ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkExtension.php: -------------------------------------------------------------------------------- 1 | load('clockwork.php'); 14 | 15 | $container->getDefinition(ClockworkSupport::class)->replaceArgument('$config', $config); 16 | } 17 | 18 | public function getConfiguration(array $config, ContainerBuilder $container) 19 | { 20 | return new ClockworkConfiguration($container->getParameter('kernel.debug')); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Clockwork/DataSource/XdebugDataSource.php: -------------------------------------------------------------------------------- 1 | xdebug = [ 'profile' => xdebug_get_profiler_filename() ]; 12 | 13 | return $request; 14 | } 15 | 16 | // Extends the request with full profiling data 17 | public function extend(Request $request) 18 | { 19 | $profile = isset($request->xdebug['profile']) ? $request->xdebug['profile'] : null; 20 | 21 | if ($profile && ! preg_match('/\.php$/', $profile) && is_readable($profile)) { 22 | $request->xdebug['profileData'] = file_get_contents($profile); 23 | 24 | if (preg_match('/\.gz$/', $profile)) { 25 | $request->xdebug['profileData'] = gzdecode($request->xdebug['profileData']); 26 | } 27 | } 28 | 29 | return $request; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Clockwork/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | $value) { 20 | $this->$key = $value; 21 | } 22 | 23 | $this->call = $this->formatCall(); 24 | 25 | $this->shortPath = $this->file ? str_replace($basePath, '', $this->file) : null; 26 | $this->vendor = ($this->file && strpos($this->file, $vendorPath) === 0) 27 | ? explode(DIRECTORY_SEPARATOR, str_replace($vendorPath, '', $this->file))[0] : null; 28 | } 29 | 30 | protected function formatCall() 31 | { 32 | if ($this->class) { 33 | return "{$this->class}{$this->type}{$this->function}()"; 34 | } else { 35 | return "{$this->function}()"; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Clockwork/Support/Slim/Old/ClockworkLogWriter.php: -------------------------------------------------------------------------------- 1 | 'emergency', 14 | 2 => 'alert', 15 | 3 => 'critical', 16 | 4 => 'error', 17 | 5 => 'warning', 18 | 6 => 'notice', 19 | 7 => 'info', 20 | 8 => 'debug' 21 | ]; 22 | 23 | public function __construct(Clockwork $clockwork, $originalLogWriter) 24 | { 25 | $this->clockwork = $clockwork; 26 | $this->originalLogWriter = $originalLogWriter; 27 | } 28 | 29 | public function write($message, $level = null) 30 | { 31 | $this->clockwork->log($this->getPsrLevel($level), $message); 32 | 33 | if ($this->originalLogWriter) { 34 | return $this->originalLogWriter->write($message, $level); 35 | } 36 | } 37 | 38 | protected function getPsrLevel($level) 39 | { 40 | return isset($this->logLevels[$level]) ? $this->logLevels[$level] : $level; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Clockwork/Support/Lumen/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | app = $app; 16 | } 17 | 18 | // Handle an incoming request 19 | public function handle($request, \Closure $next) 20 | { 21 | $this->app['clockwork']->event('Controller')->begin(); 22 | 23 | try { 24 | $response = $next($request); 25 | } catch (\Exception $e) { 26 | $this->app[ExceptionHandler::class]->report($e); 27 | $response = $this->app[ExceptionHandler::class]->render($request, $e); 28 | } 29 | 30 | return $this->app['clockwork.support']->processRequest($request, $response); 31 | } 32 | 33 | // Record the current request after a response is sent 34 | public function terminate() 35 | { 36 | $this->app['clockwork.support']->recordRequest(); 37 | } 38 | } -------------------------------------------------------------------------------- /Clockwork/Web/Web.php: -------------------------------------------------------------------------------- 1 | resolveAssetPath($path); 10 | 11 | if (! $path) return; 12 | 13 | switch (pathinfo($path, PATHINFO_EXTENSION)) { 14 | case 'css': $mime = 'text/css'; break; 15 | case 'js': $mime = 'application/javascript'; break; 16 | case 'json': $mime = 'application/json'; break; 17 | case 'png': $mime = 'image/png'; break; 18 | default: $mime = 'text/html'; break; 19 | } 20 | 21 | return [ 22 | 'path' => $path, 23 | 'mime' => $mime 24 | ]; 25 | } 26 | 27 | // Resolves absolute path of the asset, protects from accessing files outside Clockwork public dir 28 | protected function resolveAssetPath($path) 29 | { 30 | $publicPath = realpath(__DIR__ . '/public'); 31 | 32 | $path = realpath("$publicPath/{$path}"); 33 | 34 | return strpos($path, $publicPath) === 0 ? $path : false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkConfiguration.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 14 | } 15 | 16 | public function getConfigTreeBuilder() 17 | { 18 | return $this->getConfigRoot() 19 | ->children() 20 | ->booleanNode('enable')->defaultValue($this->debug)->end() 21 | ->variableNode('web')->defaultValue(true)->end() 22 | ->booleanNode('authentication')->defaultValue(false)->end() 23 | ->scalarNode('authentication_password')->defaultValue('VerySecretPassword')->end() 24 | ->end() 25 | ->end(); 26 | } 27 | 28 | protected function getConfigRoot() 29 | { 30 | if (Kernel::VERSION_ID < 40300) { 31 | return (new TreeBuilder)->root('clockwork'); 32 | } 33 | 34 | return (new TreeBuilder('clockwork'))->getRootNode(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | app = $app; 16 | } 17 | 18 | // Handle an incoming request 19 | public function handle($request, \Closure $next) 20 | { 21 | $this->app['clockwork']->event('Controller')->begin(); 22 | 23 | try { 24 | $response = $next($request); 25 | } catch (\Exception $e) { 26 | $this->app[ExceptionHandler::class]->report($e); 27 | $response = $this->app[ExceptionHandler::class]->render($request, $e); 28 | } 29 | 30 | return $this->app['clockwork.support']->processRequest($request, $response); 31 | } 32 | 33 | // Record the current request after a response is sent 34 | public function terminate() 35 | { 36 | $this->app['clockwork.support']->recordRequest(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013 Miroslav Rigler 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 | -------------------------------------------------------------------------------- /Clockwork/DataSource/MonologDataSource.php: -------------------------------------------------------------------------------- 1 | log = new Log; 20 | 21 | $handler = Logger::API == 1 ? new LegacyClockworkHandler($this->log) : new ClockworkHandler($this->log); 22 | 23 | $monolog->pushHandler($handler); 24 | } 25 | 26 | // Adds log entries to the request 27 | public function resolve(Request $request) 28 | { 29 | $request->log()->merge($this->log); 30 | 31 | return $request; 32 | } 33 | 34 | // Reset the data source to an empty state, clearing any collected data 35 | public function reset() 36 | { 37 | $this->log = new Log; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 17 | $this->profiler = $profiler; 18 | } 19 | 20 | public function clockwork() 21 | { 22 | return (new Clockwork) 23 | ->authenticator($this->container->get('clockwork.authenticator')) 24 | ->storage($this->container->get('clockwork.storage')); 25 | } 26 | 27 | public function clockworkAuthenticator() 28 | { 29 | return $this->container->get('clockwork.support')->makeAuthenticator(); 30 | } 31 | 32 | public function clockworkStorage() 33 | { 34 | return new SymfonyStorage( 35 | $this->profiler, substr($this->container->getParameter('profiler.storage.dsn'), 5) 36 | ); 37 | } 38 | 39 | public function clockworkSupport($config) 40 | { 41 | return new ClockworkSupport($this->container, $config); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Clockwork/Request/IncomingRequest.php: -------------------------------------------------------------------------------- 1 | $val) $this->$key = $val; 25 | } 26 | 27 | // Returns a header value, or default if not set 28 | public function header($key, $default = null) 29 | { 30 | return isset($this->headers[$key]) ? $this->headers[$key] : $default; 31 | } 32 | 33 | // Returns an input value, or default if not set 34 | public function input($key, $default = null) 35 | { 36 | return isset($this->input[$key]) ? $this->input[$key] : $default; 37 | } 38 | 39 | // Returns true, if HTTP host is one of the common domains used for local development 40 | public function hasLocalHost() 41 | { 42 | $segments = explode('.', $this->host); 43 | $tld = $segments[count($segments) - 1]; 44 | 45 | return $this->host == '127.0.0.1' 46 | || in_array($tld, [ 'localhost', 'local', 'test', 'wip' ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Clockwork/DataSource/SwiftDataSource.php: -------------------------------------------------------------------------------- 1 | swift = $swift; 22 | 23 | $this->timeline = new Timeline; 24 | } 25 | 26 | // Listen to the email events 27 | public function listenToEvents() 28 | { 29 | $this->swift->registerPlugin(new SwiftPluginClockworkTimeline($this->timeline)); 30 | } 31 | 32 | // Adds send emails to the request 33 | public function resolve(Request $request) 34 | { 35 | $request->emailsData = array_merge($request->emailsData, $this->timeline->finalize()); 36 | 37 | return $request; 38 | } 39 | 40 | // Reset the data source to an empty state, clearing any collected data 41 | public function reset() 42 | { 43 | $this->timeline = new Timeline; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Clockwork/Helpers/Concerns/ResolvesViewName.php: -------------------------------------------------------------------------------- 1 | first(function ($frame) { 12 | return $frame->shortPath ? preg_match('#^/storage/framework/views/[a-z0-9]+\.php$#', $frame->shortPath) : false; 13 | }); 14 | 15 | if (! $viewFrame) return $this; 16 | 17 | $renderFrame = $this->first(function ($frame) { 18 | return $frame->call == 'Illuminate\View\View->getContents()' 19 | && $frame->object instanceof \Illuminate\View\View; 20 | }); 21 | 22 | if (! $renderFrame) return $this; 23 | 24 | $resolvedViewFrame = new StackFrame( 25 | [ 'file' => $renderFrame->object->getPath(), 'line' => $viewFrame->line ], 26 | $this->basePath, 27 | $this->vendorPath 28 | ); 29 | 30 | return $this->copy(array_merge( 31 | array_slice($this->frames, 0, array_search($viewFrame, $this->frames)), 32 | [ $resolvedViewFrame ], 33 | array_slice($this->frames, array_search($viewFrame, $this->frames) + 2) 34 | )); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Clockwork/Request/UserData.php: -------------------------------------------------------------------------------- 1 | data[$key] = new UserDataItem($data); 17 | } 18 | 19 | return $this->data[] = new UserDataItem($data); 20 | } 21 | 22 | // Add user data shown as counters 23 | public function counters(array $data) 24 | { 25 | return $this->data($data) 26 | ->showAs('counters'); 27 | } 28 | 29 | // Add user data shown as table 30 | public function table($title, array $data) 31 | { 32 | return $this->data($data) 33 | ->showAs('table') 34 | ->title($title); 35 | } 36 | 37 | // Set data title 38 | public function title($title) 39 | { 40 | $this->title = $title; 41 | return $this; 42 | } 43 | 44 | // Transform data and all contents to a serializable array with metadata 45 | public function toArray() 46 | { 47 | return array_merge( 48 | array_map(function ($data) { return $data->toArray(); }, $this->data), 49 | [ '__meta' => array_filter([ 'title' => $this->title ]) ] 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Clockwork/DataSource/TwigDataSource.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 24 | } 25 | 26 | // Register the Twig profiler extension 27 | public function listenToEvents() 28 | { 29 | if (class_exists(ProfilerExtension::class)) { 30 | $this->twig->addExtension(new ProfilerExtension(($this->profile = new Profile()))); 31 | } else { 32 | $this->twig->addExtension(new Twig_Extension_Profiler($this->profile = new Twig_Profiler_Profile)); 33 | } 34 | } 35 | 36 | // Adds rendered views to the request 37 | public function resolve(Request $request) 38 | { 39 | $timeline = (new ProfilerClockworkDumper)->dump($this->profile); 40 | 41 | $request->viewsData = array_merge($request->viewsData, $timeline->finalize()); 42 | 43 | return $request; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Clockwork/Support/Twig/ProfilerClockworkDumper.php: -------------------------------------------------------------------------------- 1 | dumpProfile($profile, $timeline); 16 | 17 | return $timeline; 18 | } 19 | 20 | public function dumpProfile($profile, Timeline $timeline, $parent = null) 21 | { 22 | $id = $this->lastId++; 23 | 24 | if ($profile->isRoot()) { 25 | $name = $profile->getName(); 26 | } elseif ($profile->isTemplate()) { 27 | $name = basename($profile->getTemplate()); 28 | } else { 29 | $name = basename($profile->getTemplate()) . '::' . $profile->getType() . '(' . $profile->getName() . ')'; 30 | } 31 | 32 | foreach ($profile as $p) { 33 | $this->dumpProfile($p, $timeline, $id); 34 | } 35 | 36 | $data = $profile->__serialize(); 37 | 38 | $timeline->event($name, [ 39 | 'name' => $id, 40 | 'start' => isset($data[3]['wt']) ? $data[3]['wt'] : null, 41 | 'end' => isset($data[4]['wt']) ? $data[4]['wt'] : null, 42 | 'data' => [ 43 | 'data' => [], 44 | 'memoryUsage' => isset($data[4]['mu']) ? $data[4]['mu'] : null, 45 | 'parent' => $parent 46 | ] 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Clockwork/Helpers/ServerTiming.php: -------------------------------------------------------------------------------- 1 | metrics[] = [ 'metric' => $metric, 'value' => $value, 'description' => $description ]; 15 | 16 | return $this; 17 | } 18 | 19 | // Generate the header value 20 | public function value() 21 | { 22 | return implode(', ', array_map(function ($metric) { 23 | return "{$metric['metric']}; dur={$metric['value']}; desc=\"{$metric['description']}\""; 24 | }, $this->metrics)); 25 | } 26 | 27 | // Create a new instance from a Clockwork request 28 | public static function fromRequest(Request $request, $eventsCount = 10) 29 | { 30 | $header = new static; 31 | 32 | $header->add('app', $request->getResponseDuration(), 'Application'); 33 | 34 | if ($request->getDatabaseDuration()) { 35 | $header->add('db', $request->getDatabaseDuration(), 'Database'); 36 | } 37 | 38 | // add timeline events limited to a set number so the header doesn't get too large 39 | foreach (array_slice($request->timeline()->events, 0, $eventsCount) as $i => $event) { 40 | $header->add("timeline-event-{$i}", $event->duration(), $event->description); 41 | } 42 | 43 | return $header; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/ClockworkCleanCommand.php: -------------------------------------------------------------------------------- 1 | option('all')) { 33 | $this->laravel['config']->set('clockwork.storage_expiration', 0); 34 | } elseif ($expiration = $this->option('expiration')) { 35 | $this->laravel['config']->set('clockwork.storage_expiration', $expiration); 36 | } 37 | 38 | $this->laravel['clockwork.support']->makeStorage()->cleanup($force = true); 39 | 40 | $this->info('Metadata cleaned successfully.'); 41 | } 42 | 43 | // Compatibility for the old Laravel versions 44 | public function fire() 45 | { 46 | return $this->handle(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Clockwork/Request/UserDataItem.php: -------------------------------------------------------------------------------- 1 | data = $data; 21 | } 22 | 23 | // Set how the item should be presented ("counters" or "table") 24 | public function showAs($showAs) 25 | { 26 | $this->showAs = $showAs; 27 | return $this; 28 | } 29 | 30 | // Set data title (shown as table title in the official app) 31 | public function title($title) 32 | { 33 | $this->title = $title; 34 | return $this; 35 | } 36 | 37 | // Set a map of human-readable labels for the data contents 38 | public function labels($labels) 39 | { 40 | $this->labels = $labels; 41 | return $this; 42 | } 43 | 44 | // Transform contents to a serializable array with metadata 45 | public function toArray() 46 | { 47 | return array_merge($this->data, [ 48 | '__meta' => array_filter([ 49 | 'showAs' => $this->showAs, 50 | 'title' => $this->title, 51 | 'labels' => $this->labels 52 | ]) 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkListener.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 18 | $this->profiler = $profiler; 19 | } 20 | 21 | public function onKernelRequest(KernelEvent $event) 22 | { 23 | if (preg_match('#/__clockwork(.*)#', $event->getRequest()->getPathInfo())) { 24 | $this->profiler->disable(); 25 | } 26 | } 27 | 28 | public function onKernelResponse(KernelEvent $event) 29 | { 30 | if (! $this->clockwork->isEnabled()) return; 31 | 32 | $response = $event->getResponse(); 33 | 34 | if (! $response->headers->has('X-Debug-Token')) return; 35 | 36 | $response->headers->set('X-Clockwork-Id', $response->headers->get('X-Debug-Token')); 37 | $response->headers->set('X-Clockwork-Version', Clockwork::VERSION); 38 | } 39 | 40 | public static function getSubscribedEvents() 41 | { 42 | return [ 43 | KernelEvents::REQUEST => [ 'onKernelRequest', 512 ], 44 | KernelEvents::RESPONSE => [ 'onKernelResponse', -128 ] 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itsgoingd/clockwork", 3 | "description": "php dev tools in your browser", 4 | "keywords": ["debugging", "profiling", "logging", "laravel", "lumen", "slim", "devtools"], 5 | "homepage": "https://underground.works/clockwork", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "itsgoingd", 10 | "email": "itsgoingd@luzer.sk", 11 | "homepage": "https://twitter.com/itsgoingd" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.6", 16 | "ext-json": "*" 17 | }, 18 | "suggest": { 19 | "ext-pdo": "Needed in order to use a SQL database for metadata storage", 20 | "ext-pdo_sqlite": "Needed in order to use a SQLite for metadata storage", 21 | "ext-pdo_mysql": "Needed in order to use MySQL for metadata storage", 22 | "ext-pdo_postgres": "Needed in order to use Postgres for metadata storage", 23 | "ext-redis": "Needed in order to use Redis for metadata storage", 24 | "php-http/discovery": "Vanilla integration - required for the middleware zero-configuration setup" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Clockwork\\": "Clockwork/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Clockwork\\Support\\Laravel\\ClockworkServiceProvider" 35 | ], 36 | "aliases": { 37 | "Clockwork": "Clockwork\\Support\\Laravel\\Facade" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Clockwork/Request/ShouldRecord.php: -------------------------------------------------------------------------------- 1 | $val) $this->$key = $val; 18 | } 19 | 20 | // Apply the filter to a request 21 | public function filter(Request $request) 22 | { 23 | return $this->passErrorsOnly($request) 24 | && $this->passSlowOnly($request) 25 | && $this->passCallback($request); 26 | } 27 | 28 | protected function passErrorsOnly(Request $request) 29 | { 30 | if (! $this->errorsOnly) return true; 31 | 32 | return 400 <= $request->responseStatus && $request->responseStatus <= 599; 33 | } 34 | 35 | protected function passSlowOnly(Request $request) 36 | { 37 | if (! $this->slowOnly) return true; 38 | 39 | return $request->getResponseDuration() >= $this->slowOnly; 40 | } 41 | 42 | protected function passCallback(Request $request) 43 | { 44 | if (! $this->callback) return true; 45 | 46 | return call_user_func($this->callback, $request); 47 | } 48 | 49 | // Fluent API 50 | public function __call($method, $parameters) 51 | { 52 | if (! count($parameters)) return $this->$method; 53 | 54 | $this->$method = count($parameters) ? $parameters[0] : true; 55 | 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Clockwork/Support/Vanilla/iframe.html.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Clockwork 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | -------------------------------------------------------------------------------- /Clockwork/Storage/SymfonyStorage.php: -------------------------------------------------------------------------------- 1 | profiler = $profiler; 21 | $this->path = $path; 22 | } 23 | 24 | // Store request, no-op since this is read-only storage implementation 25 | public function store(Request $request, $skipIndex = false) 26 | { 27 | return; 28 | } 29 | 30 | // Cleanup old requests, no-op since this is read-only storage implementation 31 | public function cleanup($force = false) 32 | { 33 | return; 34 | } 35 | 36 | protected function loadRequest($token) 37 | { 38 | return ($profile = $this->profiler->loadProfile($token)) ? (new ProfileTransformer)->transform($profile) : null; 39 | } 40 | 41 | // Open index file, optionally move file pointer to the end 42 | protected function openIndex($position = 'start', $lock = null, $force = null) 43 | { 44 | $this->indexHandle = fopen("{$this->path}/index.csv", 'r'); 45 | 46 | if ($position == 'end') fseek($this->indexHandle, 0, SEEK_END); 47 | } 48 | 49 | protected function makeRequestFromIndex($record) 50 | { 51 | return new Request(array_combine( 52 | [ 'id', 'method', 'uri', 'time', 'parent', 'responseStatus' ], 53 | [ $record[0], $record[2], $record[3], $record[4], $record[5], $record[6] ] 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkLoader.php: -------------------------------------------------------------------------------- 1 | support = $support; 14 | } 15 | 16 | public function load($resource, $type = null) 17 | { 18 | $routes = new RouteCollection(); 19 | 20 | $routes->add('clockwork', new Route('/__clockwork/{id}/{direction}/{count}', [ 21 | '_controller' => [ ClockworkController::class, 'getData' ], 22 | 'direction' => null, 23 | 'count' => null 24 | ], [ 'id' => '(?!(app|auth))([a-z0-9-]+|latest)', 'direction' => '(next|previous)', 'count' => '\d+' ])); 25 | 26 | $routes->add('clockwork.auth', new Route('/__clockwork/auth', [ 27 | '_controller' => [ ClockworkController::class, 'authenticate' ] 28 | ])); 29 | 30 | if (! $this->support->isWebEnabled()) return $routes; 31 | 32 | foreach ($this->support->webPaths() as $path) { 33 | $routes->add("clockwork.webRedirect.{$path}", new Route("{$path}", [ 34 | '_controller' => [ ClockworkController::class, 'webRedirect' ] 35 | ])); 36 | 37 | $routes->add("clockwork.webIndex.{$path}", new Route("{$path}/app", [ 38 | '_controller' => [ ClockworkController::class, 'webIndex' ] 39 | ])); 40 | 41 | $routes->add("clockwork.webAsset.{$path}", new Route("{$path}/{path}", [ 42 | '_controller' => [ ClockworkController::class, 'webAsset' ] 43 | ], [ 'path' => '.+' ])); 44 | } 45 | 46 | return $routes; 47 | } 48 | 49 | public function supports($resource, $type = null) 50 | { 51 | return $type == 'clockwork'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingOldFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 16 | } 17 | 18 | public function capturedOutput() 19 | { 20 | $capturedOutput = $this->capturedOutput; 21 | 22 | $this->capturedOutput = null; 23 | 24 | return $capturedOutput; 25 | } 26 | 27 | public function setDecorated($decorated) 28 | { 29 | return $this->formatter->setDecorated($decorated); 30 | } 31 | 32 | public function isDecorated() 33 | { 34 | return $this->formatter->isDecorated(); 35 | } 36 | 37 | public function setStyle($name, OutputFormatterStyleInterface $style) 38 | { 39 | return $this->formatter->setStyle($name, $style); 40 | } 41 | 42 | public function hasStyle($name) 43 | { 44 | return $this->formatter->hasStyle($name); 45 | } 46 | 47 | public function getStyle($name) 48 | { 49 | return $this->formatter->getStyle($name); 50 | } 51 | 52 | public function format($message) 53 | { 54 | $formatted = $this->formatter->format($message); 55 | 56 | $this->capturedOutput .= $formatted; 57 | 58 | return $formatted; 59 | } 60 | 61 | public function __call($method, $args) 62 | { 63 | return $this->formatter->$method(...$args); 64 | } 65 | 66 | public function __clone() 67 | { 68 | $this->formatter = clone $this->formatter; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Clockwork/Support/Swift/SwiftPluginClockworkTimeline.php: -------------------------------------------------------------------------------- 1 | timeline = $timeline; 17 | } 18 | 19 | // Invoked immediately before a message is sent 20 | public function beforeSendPerformed(Swift_Events_SendEvent $evt) 21 | { 22 | $message = $evt->getMessage(); 23 | 24 | $headers = []; 25 | foreach ($message->getHeaders()->getAll() as $header) { 26 | $headers[$header->getFieldName()] = $header->getFieldBody(); 27 | } 28 | 29 | $this->timeline->event('Sending an email message', [ 30 | 'name' => 'email ' . $message->getId(), 31 | 'start' => $time = microtime(true), 32 | 'data' => [ 33 | 'from' => $this->addressToString($message->getFrom()), 34 | 'to' => $this->addressToString($message->getTo()), 35 | 'subject' => $message->getSubject(), 36 | 'headers' => $headers 37 | ] 38 | ]); 39 | } 40 | 41 | // Invoked immediately after a message is sent 42 | public function sendPerformed(Swift_Events_SendEvent $evt) 43 | { 44 | $message = $evt->getMessage(); 45 | 46 | $this->timeline->event('email ' . $message->getId())->end(); 47 | } 48 | 49 | protected function addressToString($address) 50 | { 51 | if (! $address) return; 52 | 53 | foreach ($address as $email => $name) { 54 | $address[$email] = $name ? "$name <$email>" : $email; 55 | } 56 | 57 | return implode(', ', $address); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 16 | } 17 | 18 | public function capturedOutput() 19 | { 20 | $capturedOutput = $this->capturedOutput; 21 | 22 | $this->capturedOutput = null; 23 | 24 | return $capturedOutput; 25 | } 26 | 27 | public function setDecorated(bool $decorated): void 28 | { 29 | $this->formatter->setDecorated($decorated); 30 | } 31 | 32 | public function isDecorated(): bool 33 | { 34 | return $this->formatter->isDecorated(); 35 | } 36 | 37 | public function setStyle(string $name, OutputFormatterStyleInterface $style): void 38 | { 39 | $this->formatter->setStyle($name, $style); 40 | } 41 | 42 | public function hasStyle(string $name): bool 43 | { 44 | return $this->formatter->hasStyle($name); 45 | } 46 | 47 | public function getStyle(string $name): OutputFormatterStyleInterface 48 | { 49 | return $this->formatter->getStyle($name); 50 | } 51 | 52 | public function format(?string $message): ?string 53 | { 54 | $formatted = $this->formatter->format($message); 55 | 56 | $this->capturedOutput .= $formatted; 57 | 58 | return $formatted; 59 | } 60 | 61 | public function __call($method, $args) 62 | { 63 | return $this->formatter->$method(...$args); 64 | } 65 | 66 | public function __clone() 67 | { 68 | $this->formatter = clone $this->formatter; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingLegacyFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 16 | } 17 | 18 | public function capturedOutput() 19 | { 20 | $capturedOutput = $this->capturedOutput; 21 | 22 | $this->capturedOutput = null; 23 | 24 | return $capturedOutput; 25 | } 26 | 27 | public function setDecorated(bool $decorated) 28 | { 29 | return $this->formatter->setDecorated($decorated); 30 | } 31 | 32 | public function isDecorated(): bool 33 | { 34 | return $this->formatter->isDecorated(); 35 | } 36 | 37 | public function setStyle(string $name, OutputFormatterStyleInterface $style) 38 | { 39 | return $this->formatter->setStyle($name, $style); 40 | } 41 | 42 | public function hasStyle(string $name): bool 43 | { 44 | return $this->formatter->hasStyle($name); 45 | } 46 | 47 | public function getStyle(string $name): OutputFormatterStyleInterface 48 | { 49 | return $this->formatter->getStyle($name); 50 | } 51 | 52 | public function format(?string $message): ?string 53 | { 54 | $formatted = $this->formatter->format($message); 55 | 56 | $this->capturedOutput .= $formatted; 57 | 58 | return $formatted; 59 | } 60 | 61 | public function __call($method, $args) 62 | { 63 | return $this->formatter->$method(...$args); 64 | } 65 | 66 | public function __clone() 67 | { 68 | $this->formatter = clone $this->formatter; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Clockwork/DataSource/DataSource.php: -------------------------------------------------------------------------------- 1 | filters[$type] = isset($this->filters[$type]) 32 | ? array_merge($this->filters[$type], [ $filter ]) : [ $filter ]; 33 | 34 | return $this; 35 | } 36 | 37 | // Clear all registered filters 38 | public function clearFilters() 39 | { 40 | $this->filters = []; 41 | 42 | return $this; 43 | } 44 | 45 | // Returns boolean whether the filterable passes all registered filters 46 | protected function passesFilters($args, $type = 'default') 47 | { 48 | $filters = isset($this->filters[$type]) ? $this->filters[$type] : []; 49 | 50 | foreach ($filters as $filter) { 51 | if (! $filter(...$args)) return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | // Censors passwords in an array, identified by key containing "pass" substring 58 | public function removePasswords(array $data) 59 | { 60 | $keys = array_keys($data); 61 | $values = array_map(function ($value, $key) { 62 | return strpos($key, 'pass') !== false ? '*removed*' : $value; 63 | }, $data, $keys); 64 | 65 | return array_combine($keys, $values); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/Resources/config/clockwork.php: -------------------------------------------------------------------------------- 1 | register(Clockwork\Support\Symfony\ClockworkFactory::class) 8 | ->setArgument('$container', new Reference('service_container')) 9 | ->setArgument('$profiler', new Reference('profiler')); 10 | 11 | $container->register(Clockwork\Clockwork::class) 12 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockwork' ]) 13 | ->setPublic(true); 14 | 15 | $container->register(Clockwork\Authentication\AuthenticatorInterface::class) 16 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkAuthenticator' ]) 17 | ->setPublic(true); 18 | 19 | $container->register(Clockwork\Storage\StorageInterface::class) 20 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkStorage' ]) 21 | ->setPublic(true); 22 | 23 | $container->register(Clockwork\Support\Symfony\ClockworkSupport::class) 24 | ->setArgument('$config', []) 25 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkSupport' ]) 26 | ->setPublic(true); 27 | 28 | $container->autowire(Clockwork\Support\Symfony\ClockworkController::class) 29 | ->setAutoconfigured(true); 30 | 31 | $container->autowire(Clockwork\Support\Symfony\ClockworkListener::class) 32 | ->setArgument('$profiler', new Reference('profiler')) 33 | ->addTag('kernel.event_subscriber'); 34 | 35 | $container->autowire(Clockwork\Support\Symfony\ClockworkLoader::class) 36 | ->addTag('routing.loader'); 37 | 38 | $container->setAlias('clockwork', Clockwork\Clockwork::class)->setPublic('true'); 39 | $container->setAlias('clockwork.authenticator', Clockwork\Authentication\AuthenticatorInterface::class)->setPublic('true'); 40 | $container->setAlias('clockwork.storage', Clockwork\Storage\StorageInterface::class)->setPublic('true'); 41 | $container->setAlias('clockwork.support', Clockwork\Support\Symfony\ClockworkSupport::class)->setPublic('true'); 42 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkController.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 17 | $this->support = $support; 18 | } 19 | 20 | public function authenticate(Request $request) 21 | { 22 | $this->ensureClockworkIsEnabled(); 23 | 24 | $token = $this->clockwork->authenticator()->attempt($request->request->all()); 25 | 26 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 27 | } 28 | 29 | public function getData(Request $request, $id = null, $direction = null, $count = null) 30 | { 31 | $this->ensureClockworkIsEnabled(); 32 | 33 | return $this->support->getData($request, $id, $direction, $count); 34 | } 35 | 36 | public function webIndex(Request $request) 37 | { 38 | $this->ensureClockworkIsEnabled(); 39 | $this->ensureClockworkWebIsEnabled(); 40 | 41 | return $this->support->getWebAsset('index.html'); 42 | } 43 | 44 | public function webAsset($path) 45 | { 46 | $this->ensureClockworkIsEnabled(); 47 | $this->ensureClockworkWebIsEnabled(); 48 | 49 | return $this->support->getWebAsset($path); 50 | } 51 | 52 | public function webRedirect(Request $request) 53 | { 54 | $this->ensureClockworkIsEnabled(); 55 | $this->ensureClockworkWebIsEnabled(); 56 | 57 | $path = $this->support->webPaths()[0]; 58 | 59 | return $this->redirectToRoute("clockwork.webIndex.{$path}"); 60 | } 61 | 62 | protected function ensureClockworkIsEnabled() 63 | { 64 | if (! $this->support->isEnabled()) throw $this->createNotFoundException(); 65 | } 66 | 67 | protected function ensureClockworkWebIsEnabled() 68 | { 69 | if (! $this->support->isWebEnabled()) throw $this->createNotFoundException(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelViewsDataSource.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 26 | 27 | $this->collectData = $collectData; 28 | 29 | $this->views = new Timeline; 30 | } 31 | 32 | // Adds rendered views to the request 33 | public function resolve(Request $request) 34 | { 35 | $request->viewsData = array_merge($request->viewsData, $this->views->finalize()); 36 | 37 | return $request; 38 | } 39 | 40 | // Reset the data source to an empty state, clearing any collected data 41 | public function reset() 42 | { 43 | $this->views = new Timeline; 44 | } 45 | 46 | // Listen to the views events 47 | public function listenToEvents() 48 | { 49 | $this->dispatcher->listen('composing:*', function ($view, $data = null) { 50 | if (is_string($view) && is_array($data)) { // Laravel 5.4 wildcard event 51 | $view = $data[0]; 52 | } 53 | 54 | $data = array_filter( 55 | $this->collectData ? $view->getData() : [], 56 | function ($v, $k) { return strpos($k, '__') !== 0; }, 57 | \ARRAY_FILTER_USE_BOTH 58 | ); 59 | 60 | $this->views->event('Rendering a view', [ 61 | 'name' => 'view ' . $view->getName(), 62 | 'start' => $time = microtime(true), 63 | 'end' => $time, 64 | 'data' => [ 65 | 'name' => $view->getName(), 66 | 'data' => (new Serializer)->normalize($data) 67 | ] 68 | ]); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Clockwork/Request/Timeline/Timeline.php: -------------------------------------------------------------------------------- 1 | create($event['description'], $event); 14 | } 15 | } 16 | 17 | // Find or create a new event, takes description and optional data - name, start, end, duration, color, data 18 | public function event($description, $data = []) 19 | { 20 | $name = isset($data['name']) ? $data['name'] : $description; 21 | 22 | if ($event = $this->find($name)) return $event; 23 | 24 | return $this->create($description, $data); 25 | } 26 | 27 | // Create a new event, takes description and optional data - name, start, end, duration, color, data 28 | public function create($description, $data = []) 29 | { 30 | return $this->events[] = new Event($description, $data); 31 | } 32 | 33 | // Find event by name 34 | public function find($name) 35 | { 36 | foreach ($this->events as $event) { 37 | if ($event->name == $name) return $event; 38 | } 39 | } 40 | 41 | // Merge another timeline instance into the current timeline 42 | public function merge(Timeline $timeline) 43 | { 44 | $this->events = array_merge($this->events, $timeline->events); 45 | 46 | return $this; 47 | } 48 | 49 | // Finalize timeline, ends all events, sorts them and returns as an array 50 | public function finalize($start = null, $end = null) 51 | { 52 | foreach ($this->events as $event) { 53 | $event->finalize($start, $end); 54 | } 55 | 56 | $this->sort(); 57 | 58 | return $this->toArray(); 59 | } 60 | 61 | // Sort the timeline events by start time 62 | public function sort() 63 | { 64 | usort($this->events, function ($a, $b) { return $a->start * 1000 - $b->start * 1000; }); 65 | } 66 | 67 | // Return events as an array 68 | public function toArray() 69 | { 70 | return array_map(function ($event) { return $event->toArray(); }, $this->events); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Clockwork/Support/Lumen/ClockworkSupport.php: -------------------------------------------------------------------------------- 1 | app = $app; 17 | } 18 | 19 | // Resolves the framework data source from the container 20 | protected function frameworkDataSource() 21 | { 22 | return $this->app['clockwork.lumen']; 23 | } 24 | 25 | // Process an http request and response, resolves the request, sets Clockwork headers and cookies 26 | public function process($request, $response) 27 | { 28 | if (! $response instanceof Response) { 29 | $response = new Response((string) $response); 30 | } 31 | 32 | return parent::process($request, $response); 33 | } 34 | 35 | // Set response on the framework data source 36 | protected function setResponse($response) 37 | { 38 | $this->app['clockwork.lumen']->setResponse($response); 39 | } 40 | 41 | // Check whether Clockwork is enabled 42 | public function isEnabled() 43 | { 44 | return $this->getConfig('enable') 45 | || $this->getConfig('enable') === null && env('APP_DEBUG', false); 46 | } 47 | 48 | // Check whether a particular feature is available 49 | public function isFeatureAvailable($feature) 50 | { 51 | if ($feature == 'database') { 52 | return $this->app->bound('db') && $this->app['config']->get('database.default'); 53 | } elseif ($feature == 'emails') { 54 | return $this->app->bound('mailer'); 55 | } elseif ($feature == 'redis') { 56 | return $this->app->bound('redis') && method_exists(\Illuminate\Redis\RedisManager::class, 'enableEvents'); 57 | } elseif ($feature == 'queue') { 58 | return $this->app->bound('queue') && method_exists(\Illuminate\Queue\Queue::class, 'createPayloadUsing'); 59 | } elseif ($feature == 'xdebug') { 60 | return in_array('xdebug', get_loaded_extensions()); 61 | } 62 | 63 | return true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Clockwork/DataSource/Concerns/EloquentDetectDuplicateQueries.php: -------------------------------------------------------------------------------- 1 | duplicateQueries as $query) { 19 | if ($query['count'] <= 1) continue; 20 | 21 | $log->warning( 22 | "N+1 queries: {$query['model']}::{$query['relation']} loaded {$query['count']} times.", 23 | [ 'performance' => true, 'trace' => $query['trace'] ] 24 | ); 25 | } 26 | 27 | $request->log()->merge($log); 28 | } 29 | 30 | protected function detectDuplicateQuery(StackTrace $trace) 31 | { 32 | $relationFrame = $trace->first(function ($frame) { 33 | return $frame->function == 'getRelationValue' 34 | || $frame->class == \Illuminate\Database\Eloquent\Relations\Relation::class; 35 | }); 36 | 37 | if (! $relationFrame || ! $relationFrame->object) return; 38 | 39 | if ($relationFrame->class == \Illuminate\Database\Eloquent\Relations\Relation::class) { 40 | $model = get_class($relationFrame->object->getParent()); 41 | $relation = get_class($relationFrame->object->getRelated()); 42 | } else { 43 | $model = get_class($relationFrame->object); 44 | $relation = $relationFrame->args[0]; 45 | } 46 | 47 | $shortTrace = $trace->skip(StackFilter::make() 48 | ->isNotVendor([ 'itsgoingd', 'laravel', 'illuminate' ]) 49 | ->isNotNamespace([ 'Clockwork', 'Illuminate' ])); 50 | 51 | $hash = implode('-', [ $model, $relation, $shortTrace->first()->file, $shortTrace->first()->line ]); 52 | 53 | if (! isset($this->duplicateQueries[$hash])) { 54 | $this->duplicateQueries[$hash] = [ 55 | 'count' => 0, 56 | 'model' => $model, 57 | 'relation' => $relation, 58 | 'trace' => $trace 59 | ]; 60 | } 61 | 62 | $this->duplicateQueries[$hash]['count']++; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelQueueDataSource.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 25 | } 26 | 27 | // Adds dispatched queue jobs to the request 28 | public function resolve(Request $request) 29 | { 30 | $request->queueJobs = array_merge($request->queueJobs, $this->getJobs()); 31 | 32 | return $request; 33 | } 34 | 35 | // Reset the data source to an empty state, clearing any collected data 36 | public function reset() 37 | { 38 | $this->jobs = []; 39 | } 40 | 41 | // Listen to the queue events 42 | public function listenToEvents() 43 | { 44 | $this->queue->createPayloadUsing(function ($connection, $queue, $payload) { 45 | $this->registerJob([ 46 | 'id' => $id = (new Request)->id, 47 | 'connection' => $connection, 48 | 'queue' => $queue, 49 | 'name' => $payload['displayName'], 50 | 'data' => isset($payload['data']['command']) ? $payload['data']['command'] : null, 51 | 'maxTries' => $payload['maxTries'], 52 | 'timeout' => $payload['timeout'], 53 | 'time' => microtime(true) 54 | ]); 55 | 56 | return [ 'clockwork_id' => $id, 'clockwork_parent_id' => $this->currentRequestId ]; 57 | }); 58 | } 59 | 60 | // Set Clockwork ID of the current request 61 | public function setCurrentRequestId($requestId) 62 | { 63 | $this->currentRequestId = $requestId; 64 | return $this; 65 | } 66 | 67 | // Collect a dispatched queue job 68 | protected function registerJob(array $job) 69 | { 70 | $trace = StackTrace::get()->resolveViewName(); 71 | 72 | $job = array_merge($job, [ 73 | 'trace' => (new Serializer)->trace($trace) 74 | ]); 75 | 76 | if ($this->passesFilters([ $job ])) { 77 | $this->jobs[] = $job; 78 | } 79 | } 80 | 81 | // Get an array of dispatched queue jobs commands 82 | protected function getJobs() 83 | { 84 | return array_map(function ($query) { 85 | return array_merge($query, [ 86 | 'data' => isset($query['data']) ? (new Serializer)->normalize($query['data']) : null 87 | ]); 88 | }, $this->jobs); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelRedisDataSource.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 25 | 26 | $this->skipCacheCommands = $skipCacheCommands; 27 | 28 | if ($this->skipCacheCommands) { 29 | $this->addFilter(function ($command, $trace) { 30 | return ! $trace->first(function ($frame) { return $frame->class == 'Illuminate\Cache\RedisStore'; }); 31 | }); 32 | } 33 | } 34 | 35 | // Adds redis commands to the request 36 | public function resolve(Request $request) 37 | { 38 | $request->redisCommands = array_merge($request->redisCommands, $this->getCommands()); 39 | 40 | return $request; 41 | } 42 | 43 | // Reset the data source to an empty state, clearing any collected data 44 | public function reset() 45 | { 46 | $this->commands = []; 47 | } 48 | 49 | // Listen to the cache events 50 | public function listenToEvents() 51 | { 52 | $this->eventDispatcher->listen(\Illuminate\Redis\Events\CommandExecuted::class, function ($event) { 53 | $this->registerCommand([ 54 | 'command' => $event->command, 55 | 'parameters' => $event->parameters, 56 | 'duration' => $event->time, 57 | 'connection' => $event->connectionName, 58 | 'time' => microtime(true) - $event->time / 1000 59 | ]); 60 | }); 61 | } 62 | 63 | // Collect an executed command 64 | protected function registerCommand(array $command) 65 | { 66 | $trace = StackTrace::get()->resolveViewName(); 67 | 68 | $command = array_merge($command, [ 69 | 'trace' => (new Serializer)->trace($trace) 70 | ]); 71 | 72 | if ($this->passesFilters([ $command, $trace ])) { 73 | $this->commands[] = $command; 74 | } 75 | } 76 | 77 | // Get an array of executed redis commands 78 | protected function getCommands() 79 | { 80 | return array_map(function ($query) { 81 | return array_merge($query, [ 82 | 'parameters' => isset($query['parameters']) ? (new Serializer)->normalize($query['parameters']) : null 83 | ]); 84 | }, $this->commands); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Clockwork/Support/Lumen/Controller.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 22 | $this->clockworkSupport = $clockworkSupport; 23 | } 24 | 25 | // Authantication endpoint 26 | public function authenticate(Request $request) 27 | { 28 | $this->ensureClockworkIsEnabled(); 29 | 30 | $token = $this->clockwork->authenticator()->attempt( 31 | $request->only([ 'username', 'password' ]) 32 | ); 33 | 34 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 35 | } 36 | 37 | // Metadata retrieving endpoint 38 | public function getData(Request $request, $id = null, $direction = null, $count = null) 39 | { 40 | $this->ensureClockworkIsEnabled(); 41 | 42 | return $this->clockworkSupport->getData( 43 | $id, $direction, $count, $request->only([ 'only', 'except' ]) 44 | ); 45 | } 46 | 47 | // Extended metadata retrieving endpoint 48 | public function getExtendedData(Request $request, $id = null) 49 | { 50 | $this->ensureClockworkIsEnabled(); 51 | 52 | return $this->clockworkSupport->getExtendedData( 53 | $id, $request->only([ 'only', 'except' ]) 54 | ); 55 | } 56 | 57 | // Metadata updating endpoint 58 | public function updateData(Request $request, $id = null) 59 | { 60 | $this->ensureClockworkIsEnabled(); 61 | 62 | return $this->clockworkSupport->updateData($id, $request->json()->all()); 63 | } 64 | 65 | // App index 66 | public function webIndex(Request $request) 67 | { 68 | $this->ensureClockworkIsEnabled(); 69 | 70 | return $this->clockworkSupport->getWebAsset('index.html'); 71 | } 72 | 73 | // App assets serving 74 | public function webAsset($path) 75 | { 76 | $this->ensureClockworkIsEnabled(); 77 | 78 | return $this->clockworkSupport->getWebAsset($path); 79 | } 80 | 81 | // App redirect (/clockwork -> /clockwork/app) 82 | public function webRedirect(Request $request) 83 | { 84 | $this->ensureClockworkIsEnabled(); 85 | 86 | return new RedirectResponse('/' . $request->path() . '/app'); 87 | } 88 | 89 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present 90 | protected function ensureClockworkIsEnabled() 91 | { 92 | if (class_exists(Telescope::class)) Telescope::stopRecording(); 93 | 94 | if (! $this->clockworkSupport->isEnabled()) abort(404); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/ClockworkController.php: -------------------------------------------------------------------------------- 1 | ensureClockworkIsEnabled($clockworkSupport); 19 | 20 | $token = $clockwork->authenticator()->attempt( 21 | $request->only([ 'username', 'password' ]) 22 | ); 23 | 24 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 25 | } 26 | 27 | // Metadata retrieving endpoint 28 | public function getData(ClockworkSupport $clockworkSupport, Request $request, $id = null, $direction = null, $count = null) 29 | { 30 | $this->ensureClockworkIsEnabled($clockworkSupport); 31 | 32 | return $clockworkSupport->getData( 33 | $id, $direction, $count, $request->only([ 'only', 'except' ]) 34 | ); 35 | } 36 | 37 | // Extended metadata retrieving endpoint 38 | public function getExtendedData(ClockworkSupport $clockworkSupport, Request $request, $id = null) 39 | { 40 | $this->ensureClockworkIsEnabled($clockworkSupport); 41 | 42 | return $clockworkSupport->getExtendedData( 43 | $id, $request->only([ 'only', 'except' ]) 44 | ); 45 | } 46 | 47 | // Metadata updating endpoint 48 | public function updateData(ClockworkSupport $clockworkSupport, Request $request, $id = null) 49 | { 50 | $this->ensureClockworkIsEnabled($clockworkSupport); 51 | 52 | return $clockworkSupport->updateData($id, $request->json()->all()); 53 | } 54 | 55 | // App index 56 | public function webIndex(ClockworkSupport $clockworkSupport) 57 | { 58 | $this->ensureClockworkIsEnabled($clockworkSupport); 59 | 60 | return $clockworkSupport->getWebAsset('index.html'); 61 | } 62 | 63 | // App assets serving 64 | public function webAsset(ClockworkSupport $clockworkSupport, $path) 65 | { 66 | $this->ensureClockworkIsEnabled($clockworkSupport); 67 | 68 | return $clockworkSupport->getWebAsset($path); 69 | } 70 | 71 | // App redirect (/clockwork -> /clockwork/app) 72 | public function webRedirect(ClockworkSupport $clockworkSupport, Request $request) 73 | { 74 | $this->ensureClockworkIsEnabled($clockworkSupport); 75 | 76 | return new RedirectResponse('/' . $request->path() . '/app'); 77 | } 78 | 79 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present 80 | protected function ensureClockworkIsEnabled(ClockworkSupport $clockworkSupport) 81 | { 82 | if (class_exists(Telescope::class)) Telescope::stopRecording(); 83 | 84 | if (! $clockworkSupport->isEnabled()) abort(404); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Tests/UsesClockwork.php: -------------------------------------------------------------------------------- 1 | [] 16 | ]; 17 | 18 | // Set up Clockwork in this test case, should be called from the PHPUnit setUp method 19 | protected function setUpClockwork() 20 | { 21 | if (! $this->app->make('clockwork.support')->isCollectingTests()) return; 22 | 23 | $this->beforeApplicationDestroyed(function () { 24 | if ($this->app->make('clockwork.support')->isTestFiltered($this->toString())) return; 25 | 26 | $this->app->make('clockwork') 27 | ->resolveAsTest( 28 | $this->toString(), 29 | $this->resolveClockworkStatus(), 30 | $this->getStatusMessage(), 31 | $this->resolveClockworkAsserts() 32 | ) 33 | ->storeRequest(); 34 | }); 35 | } 36 | 37 | // Resolve Clockwork test status 38 | protected function resolveClockworkStatus() 39 | { 40 | $status = $this->getStatus(); 41 | 42 | $statuses = [ 43 | BaseTestRunner::STATUS_UNKNOWN => 'unknown', 44 | BaseTestRunner::STATUS_PASSED => 'passed', 45 | BaseTestRunner::STATUS_SKIPPED => 'skipped', 46 | BaseTestRunner::STATUS_INCOMPLETE => 'incomplete', 47 | BaseTestRunner::STATUS_FAILURE => 'failed', 48 | BaseTestRunner::STATUS_ERROR => 'error', 49 | BaseTestRunner::STATUS_RISKY => 'passed', 50 | BaseTestRunner::STATUS_WARNING => 'warning' 51 | ]; 52 | 53 | return isset($statuses[$status]) ? $statuses[$status] : null; 54 | } 55 | 56 | // Resolve executed asserts 57 | protected function resolveClockworkAsserts() 58 | { 59 | $asserts = static::$clockwork['asserts']; 60 | 61 | if ($this->getStatus() == BaseTestRunner::STATUS_FAILURE && count($asserts)) { 62 | $asserts[count($asserts) - 1]['passed'] = false; 63 | } 64 | 65 | static::$clockwork['asserts'] = []; 66 | 67 | return $asserts; 68 | } 69 | 70 | // Overload the main PHPUnit assert method to collect executed asserts 71 | public static function assertThat($value, Constraint $constraint, string $message = ''): void 72 | { 73 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]); 74 | 75 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last(); 76 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3); 77 | 78 | static::$clockwork['asserts'][] = [ 79 | 'name' => $assertFrame->function, 80 | 'arguments' => $assertFrame->args, 81 | 'trace' => (new Serializer)->trace($trace), 82 | 'passed' => true 83 | ]; 84 | 85 | parent::assertThat($value, $constraint, $message); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Clockwork/Request/Timeline/Event.php: -------------------------------------------------------------------------------- 1 | description = $description; 24 | $this->name = isset($data['name']) ? $data['name'] : $description; 25 | 26 | $this->start = isset($data['start']) ? $data['start'] : null; 27 | $this->end = isset($data['end']) ? $data['end'] : null; 28 | 29 | $this->color = isset($data['color']) ? $data['color'] : null; 30 | $this->data = isset($data['data']) ? $data['data'] : null; 31 | } 32 | 33 | // Begin the event at current time 34 | public function begin() 35 | { 36 | $this->start = microtime(true); 37 | 38 | return $this; 39 | } 40 | 41 | // End the event at current time 42 | public function end() 43 | { 44 | $this->end = microtime(true); 45 | 46 | return $this; 47 | } 48 | 49 | // Begin the event, execute the passed in closure and end the event, returns the closure return value 50 | public function run(\Closure $closure, ...$args) 51 | { 52 | $this->begin(); 53 | try { 54 | return $closure(...$args); 55 | } finally { 56 | $this->end(); 57 | } 58 | } 59 | 60 | // Set or retrieve event duration (in ms), event can be defined with both start and end time or just a single time and duration 61 | public function duration($duration = null) 62 | { 63 | if (! $duration) return ($this->start && $this->end) ? ($this->end - $this->start) * 1000 : 0; 64 | 65 | if ($this->start) $this->end = $this->start + $duration / 1000; 66 | if ($this->end) $this->start = $this->end - $duration / 1000; 67 | 68 | return $this; 69 | } 70 | 71 | // Finalize the event, ends the event, fills in start time if empty and limits the start and end time 72 | public function finalize($start = null, $end = null) 73 | { 74 | $end = $end ?: microtime(true); 75 | 76 | $this->start = $this->start ?: $start; 77 | $this->end = $this->end ?: $end; 78 | 79 | if ($this->start < $start) $this->start = $start; 80 | if ($this->end > $end) $this->end = $end; 81 | } 82 | 83 | // Fluent API 84 | public function __call($method, $parameters) 85 | { 86 | if (! count($parameters)) return $this->$method; 87 | 88 | $this->$method = $parameters[0]; 89 | 90 | return $this; 91 | } 92 | 93 | // Return an array representation of the event 94 | public function toArray() 95 | { 96 | return [ 97 | 'description' => $this->description, 98 | 'start' => $this->start, 99 | 'end' => $this->end, 100 | 'duration' => $this->duration(), 101 | 'color' => $this->color, 102 | 'data' => $this->data 103 | ]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Clockwork/DataSource/PsrMessageDataSource.php: -------------------------------------------------------------------------------- 1 | psrRequest = $psrRequest; 21 | $this->psrResponse = $psrResponse; 22 | } 23 | 24 | // Adds request and response information to the request 25 | public function resolve(Request $request) 26 | { 27 | if ($this->psrRequest) { 28 | $request->method = $this->psrRequest->getMethod(); 29 | $request->uri = $this->getRequestUri(); 30 | $request->headers = $this->getRequestHeaders(); 31 | $request->getData = $this->sanitize($this->psrRequest->getQueryParams()); 32 | $request->postData = $this->sanitize($this->psrRequest->getParsedBody()); 33 | $request->cookies = $this->sanitize($this->psrRequest->getCookieParams()); 34 | $request->time = $this->getRequestTime(); 35 | } 36 | 37 | if ($this->psrResponse !== null) { 38 | $request->responseStatus = $this->psrResponse->getStatusCode(); 39 | $request->responseTime = $this->getResponseTime(); 40 | } 41 | 42 | return $request; 43 | } 44 | 45 | // Normalize items in the array and remove passwords 46 | protected function sanitize($data) 47 | { 48 | return is_array($data) ? $this->removePasswords((new Serializer)->normalizeEach($data)) : $data; 49 | } 50 | 51 | // Get the response time, fetching it from ServerParams 52 | protected function getRequestTime() 53 | { 54 | $env = $this->psrRequest->getServerParams(); 55 | 56 | if (isset($env['REQUEST_TIME_FLOAT'])) { 57 | return $env['REQUEST_TIME_FLOAT']; 58 | } 59 | } 60 | 61 | // Get the response time (current time, assuming most of the application code has already run at this point) 62 | protected function getResponseTime() 63 | { 64 | return microtime(true); 65 | } 66 | 67 | // Get the request headers 68 | protected function getRequestHeaders() 69 | { 70 | $headers = []; 71 | 72 | foreach ($this->psrRequest->getHeaders() as $header => $values) { 73 | if (strtoupper(substr($header, 0, 5)) === 'HTTP_') { 74 | $header = substr($header, 5); 75 | } 76 | 77 | $header = str_replace('_', ' ', $header); 78 | $header = ucwords(strtolower($header)); 79 | $header = str_replace(' ', '-', $header); 80 | 81 | $headers[$header] = $values; 82 | } 83 | 84 | ksort($headers); 85 | 86 | return $headers; 87 | } 88 | 89 | // Get the request URI 90 | protected function getRequestUri() 91 | { 92 | $uri = $this->psrRequest->getUri(); 93 | 94 | return $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : ''); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Clockwork/Support/Slim/Old/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | storagePathOrClockwork = $storagePathOrClockwork; 19 | } 20 | 21 | public function call() 22 | { 23 | $this->app->container->singleton('clockwork', function () { 24 | if ($this->storagePathOrClockwork instanceof Clockwork) { 25 | return $this->storagePathOrClockwork; 26 | } 27 | 28 | $clockwork = new Clockwork(); 29 | 30 | $clockwork->addDataSource(new PhpDataSource()) 31 | ->addDataSource(new SlimDataSource($this->app)) 32 | ->storage(new FileStorage($this->storagePathOrClockwork)); 33 | 34 | return $clockwork; 35 | }); 36 | 37 | $originalLogWriter = $this->app->getLog()->getWriter(); 38 | $clockworkLogWriter = new ClockworkLogWriter($this->app->clockwork, $originalLogWriter); 39 | 40 | $this->app->getLog()->setWriter($clockworkLogWriter); 41 | 42 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; 43 | if ($this->app->config('debug') && preg_match($clockworkDataUri, $this->app->request()->getPathInfo(), $matches)) { 44 | $matches = array_merge([ 'direction' => null, 'count' => null ], $matches); 45 | return $this->retrieveRequest($matches['id'], $matches['direction'], $matches['count']); 46 | } 47 | 48 | try { 49 | $this->next->call(); 50 | $this->logRequest(); 51 | } catch (Exception $e) { 52 | $this->logRequest(); 53 | throw $e; 54 | } 55 | } 56 | 57 | public function retrieveRequest($id = null, $direction = null, $count = null) 58 | { 59 | $storage = $this->app->clockwork->storage(); 60 | 61 | if ($direction == 'previous') { 62 | $data = $storage->previous($id, $count); 63 | } elseif ($direction == 'next') { 64 | $data = $storage->next($id, $count); 65 | } elseif ($id == 'latest') { 66 | $data = $storage->latest(); 67 | } else { 68 | $data = $storage->find($id); 69 | } 70 | 71 | echo json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR); 72 | } 73 | 74 | protected function logRequest() 75 | { 76 | $this->app->clockwork->resolveRequest(); 77 | $this->app->clockwork->storeRequest(); 78 | 79 | if ($this->app->config('debug')) { 80 | $this->app->response()->header('X-Clockwork-Id', $this->app->clockwork->request()->id); 81 | $this->app->response()->header('X-Clockwork-Version', Clockwork::VERSION); 82 | 83 | $env = $this->app->environment(); 84 | if ($env['SCRIPT_NAME']) { 85 | $this->app->response()->header('X-Clockwork-Path', $env['SCRIPT_NAME'] . '/__clockwork/'); 86 | } 87 | 88 | $request = $this->app->clockwork->request(); 89 | $this->app->response()->header('Server-Timing', ServerTiming::fromRequest($request)->value()); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Clockwork/DataSource/SlimDataSource.php: -------------------------------------------------------------------------------- 1 | slim = $slim; 18 | } 19 | 20 | // Adds request and response information to the request 21 | public function resolve(Request $request) 22 | { 23 | $request->method = $this->getRequestMethod(); 24 | $request->uri = $this->getRequestUri(); 25 | $request->controller = $this->getController(); 26 | $request->headers = $this->getRequestHeaders(); 27 | $request->responseStatus = $this->getResponseStatus(); 28 | 29 | return $request; 30 | } 31 | 32 | // Get a textual representation of current route's controller 33 | protected function getController() 34 | { 35 | $matchedRoutes = $this->slim->router()->getMatchedRoutes( 36 | $this->slim->request()->getMethod(), $this->slim->request()->getResourceUri() 37 | ); 38 | 39 | if (! count($matchedRoutes)) return; 40 | 41 | $controller = end($matchedRoutes)->getCallable(); 42 | 43 | if ($controller instanceof \Closure) { 44 | $controller = 'anonymous function'; 45 | } elseif (is_object($controller)) { 46 | $controller = 'instance of ' . get_class($controller); 47 | } elseif (is_array($controller) && count($controller) == 2) { 48 | if (is_object($controller[0])) { 49 | $controller = get_class($controller[0]) . '->' . $controller[1]; 50 | } else { 51 | $controller = $controller[0] . '::' . $controller[1]; 52 | } 53 | } elseif (! is_string($controller)) { 54 | $controller = null; 55 | } 56 | 57 | return $controller; 58 | } 59 | 60 | // Get the request headers 61 | protected function getRequestHeaders() 62 | { 63 | $headers = []; 64 | 65 | foreach ($_SERVER as $key => $value) { 66 | if (substr($key, 0, 5) !== 'HTTP_') continue; 67 | 68 | $header = substr($key, 5); 69 | $header = str_replace('_', ' ', $header); 70 | $header = ucwords(strtolower($header)); 71 | $header = str_replace(' ', '-', $header); 72 | 73 | $value = $this->slim->request()->headers($header, $value); 74 | 75 | if (! isset($headers[$header])) { 76 | $headers[$header] = [ $value ]; 77 | } else { 78 | $headers[$header][] = $value; 79 | } 80 | } 81 | 82 | ksort($headers); 83 | 84 | return $headers; 85 | } 86 | 87 | // Get the request method 88 | protected function getRequestMethod() 89 | { 90 | return $this->slim->request()->getMethod(); 91 | } 92 | 93 | // Get the request URI 94 | protected function getRequestUri() 95 | { 96 | return $this->slim->request()->getPathInfo(); 97 | } 98 | 99 | // Get the response status code 100 | protected function getResponseStatus() 101 | { 102 | return $this->slim->response()->status(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkSupport.php: -------------------------------------------------------------------------------- 1 | container = $container; 22 | $this->config = $config; 23 | } 24 | 25 | public function getConfig($key, $default = null) 26 | { 27 | return isset($this->config[$key]) ? $this->config[$key] : $default; 28 | } 29 | 30 | public function getData(Request $request, $id = null, $direction = null, $count = null) 31 | { 32 | $authenticator = $this->container->get('clockwork')->authenticator(); 33 | $storage = $this->container->get('clockwork')->storage(); 34 | 35 | $authenticated = $authenticator->check($request->headers->get('X-Clockwork-Auth')); 36 | 37 | if ($authenticated !== true) { 38 | return new JsonResponse([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403); 39 | } 40 | 41 | if ($direction == 'previous') { 42 | $data = $storage->previous($id, $count, Search::fromRequest($request->query->all())); 43 | } elseif ($direction == 'next') { 44 | $data = $storage->next($id, $count, Search::fromRequest($request->query->all())); 45 | } elseif ($id == 'latest') { 46 | $data = $storage->latest(Search::fromRequest($request->query->all())); 47 | } else { 48 | $data = $storage->find($id); 49 | } 50 | 51 | $data = is_array($data) 52 | ? array_map(function ($request) { return $request->toArray(); }, $data) 53 | : $data->toArray(); 54 | 55 | return new JsonResponse($data); 56 | } 57 | 58 | public function getWebAsset($path) 59 | { 60 | $web = new Web; 61 | 62 | if ($asset = $web->asset($path)) { 63 | return new BinaryFileResponse($asset['path'], 200, [ 'Content-Type' => $asset['mime'] ]); 64 | } else { 65 | throw new NotFoundHttpException; 66 | } 67 | } 68 | 69 | public function makeAuthenticator() 70 | { 71 | $authenticator = $this->getConfig('authentication'); 72 | 73 | if (is_string($authenticator)) { 74 | return $this->container->get($authenticator); 75 | } elseif ($authenticator) { 76 | return new SimpleAuthenticator($this->getConfig('authentication_password')); 77 | } else { 78 | return new NullAuthenticator; 79 | } 80 | } 81 | 82 | public function isEnabled() 83 | { 84 | return $this->getConfig('enable', false); 85 | } 86 | 87 | public function isWebEnabled() 88 | { 89 | return $this->getConfig('web', true); 90 | } 91 | 92 | public function webPaths() 93 | { 94 | $path = $this->getConfig('web', true); 95 | 96 | if (is_string($path)) return [ trim($path, '/') ]; 97 | 98 | return [ 'clockwork', '__clockwork' ]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Clockwork/Support/Lumen/ClockworkServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->configure('clockwork'); 15 | $this->mergeConfigFrom(__DIR__ . '/../Laravel/config/clockwork.php', 'clockwork'); 16 | } 17 | 18 | // Register Clockwork components 19 | protected function registerClockwork() 20 | { 21 | parent::registerClockwork(); 22 | 23 | $this->app->singleton('clockwork.support', function ($app) { 24 | return new ClockworkSupport($app); 25 | }); 26 | 27 | if ($this->isRunningWithFacades() && ! class_exists('Clockwork')) { 28 | class_alias(\Clockwork\Support\Laravel\Facade::class, 'Clockwork'); 29 | } 30 | } 31 | 32 | // Register Clockwork data sources 33 | protected function registerDataSources() 34 | { 35 | parent::registerDataSources(); 36 | 37 | $this->app->singleton('clockwork.lumen', function ($app) { 38 | return (new LumenDataSource( 39 | $app, 40 | $app['clockwork.support']->isFeatureEnabled('log'), 41 | $app['clockwork.support']->isFeatureEnabled('views'), 42 | $app['clockwork.support']->isFeatureEnabled('routes') 43 | )); 44 | }); 45 | } 46 | 47 | // Register Clockwork components aliases for type hinting 48 | protected function registerAliases() 49 | { 50 | parent::registerAliases(); 51 | 52 | $this->app->alias('clockwork.lumen', LumenDataSource::class); 53 | } 54 | 55 | // Register event listeners 56 | protected function registerEventListeners() 57 | { 58 | $this->app['clockwork.support']->addDataSources()->listenToEvents(); 59 | } 60 | 61 | // Register Clockwork middleware 62 | public function registerMiddleware() 63 | { 64 | $this->app->middleware([ ClockworkMiddleware::class ]); 65 | } 66 | 67 | // Register Clockwork REST api routes 68 | public function registerRoutes() 69 | { 70 | $router = isset($this->app->router) ? $this->app->router : $this->app; 71 | 72 | $router->get('/__clockwork/{id:(?:[0-9-]+|latest)}/extended', 'Clockwork\Support\Lumen\Controller@getExtendedData'); 73 | $router->get('/__clockwork/{id:(?:[0-9-]+|latest)}[/{direction:(?:next|previous)}[/{count:\d+}]]', 'Clockwork\Support\Lumen\Controller@getData'); 74 | $router->put('/__clockwork/{id}', 'Clockwork\Support\Lumen\Controller@updateData'); 75 | $router->post('/__clockwork/auth', 'Clockwork\Support\Lumen\Controller@authenticate'); 76 | } 77 | 78 | // Register Clockwork app routes 79 | public function registerWebRoutes() 80 | { 81 | $router = isset($this->app->router) ? $this->app->router : $this->app; 82 | 83 | $this->app['clockwork.support']->webPaths()->each(function ($path) use ($router) { 84 | $router->get("{$path}", 'Clockwork\Support\Lumen\Controller@webRedirect'); 85 | $router->get("{$path}/app", 'Clockwork\Support\Lumen\Controller@webIndex'); 86 | $router->get("{$path}/{path:.+}", 'Clockwork\Support\Lumen\Controller@webAsset'); 87 | }); 88 | } 89 | 90 | // Check whether we are running with facades enabled 91 | protected function isRunningWithFacades() 92 | { 93 | return Facade::getFacadeApplication() !== null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Tests/ClockworkExtension.php: -------------------------------------------------------------------------------- 1 | registerSubscribers( 22 | new class implements Event\Test\PreparedSubscriber { 23 | public function notify($event): void { ClockworkExtension::$asserts = []; } 24 | }, 25 | new class implements Event\Test\ErroredSubscriber { 26 | public function notify($event): void { ClockworkExtension::recordTest('error', $event->throwable()->message()); } 27 | }, 28 | new class implements Event\Test\FailedSubscriber { 29 | public function notify($event): void { ClockworkExtension::recordTest('failed', $event->throwable()->message()); } 30 | }, 31 | new class implements Event\Test\MarkedIncompleteSubscriber { 32 | public function notify($event): void { ClockworkExtension::recordTest('incomplete', $event->throwable()->message()); } 33 | }, 34 | new class implements Event\Test\PassedSubscriber { 35 | public function notify($event): void { ClockworkExtension::recordTest('passed'); } 36 | }, 37 | new class implements Event\Test\SkippedSubscriber { 38 | public function notify($event): void { ClockworkExtension::recordTest('skipped', $event->message()); } 39 | }, 40 | new class implements Event\Test\AssertionSucceededSubscriber { 41 | public function notify($event): void { ClockworkExtension::recordAssertion(true); } 42 | }, 43 | new class implements Event\Test\AssertionFailedSubscriber { 44 | public function notify($event): void { ClockworkExtension::recordAssertion(false); } 45 | } 46 | ); 47 | } 48 | 49 | public static function recordTest($status, $message = null) 50 | { 51 | $trace = StackTrace::get([ 'arguments' => false, 'limit' => 10 ]); 52 | $testFrame = $trace->filter(function ($frame) { return $frame->object instanceof \PHPUnit\Framework\TestCase; })->last(); 53 | 54 | if (! $testFrame) return; 55 | 56 | $testInstance = $testFrame->object; 57 | 58 | $reflectionClass = new \ReflectionClass($testInstance); 59 | 60 | if (! $reflectionClass->hasProperty('app')) return; 61 | 62 | $reflectionProperty = $reflectionClass->getProperty('app'); 63 | $reflectionProperty->setAccessible(true); 64 | 65 | $app = $reflectionProperty->getValue($testInstance); 66 | 67 | if (! $app->make('clockwork.support')->isCollectingTests()) return; 68 | if ($app->make('clockwork.support')->isTestFiltered($testInstance->toString())) return; 69 | 70 | $app->make('clockwork') 71 | ->resolveAsTest( 72 | str_replace('__pest_evaluable_', '', $testInstance->toString()), 73 | $status, 74 | $message, 75 | ClockworkExtension::$asserts 76 | ) 77 | ->storeRequest(); 78 | } 79 | 80 | public static function recordAssertion($passed = true) 81 | { 82 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]); 83 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last(); 84 | 85 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3); 86 | 87 | static::$asserts[] = [ 88 | 'name' => $assertFrame->function, 89 | 'arguments' => $assertFrame->args, 90 | 'trace' => (new Serializer)->trace($trace), 91 | 'passed' => $passed 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Clockwork/Request/ShouldCollect.php: -------------------------------------------------------------------------------- 1 | except = array_merge($this->except, is_array($uris) ? $uris : [ $uris ]); 26 | 27 | return $this; 28 | } 29 | 30 | // Append one or more only URIs 31 | public function only($uris) 32 | { 33 | $this->only = array_merge($this->only, is_array($uris) ? $uris : [ $uris ]); 34 | 35 | return $this; 36 | } 37 | 38 | // Merge multiple settings from array 39 | public function merge(array $data = []) 40 | { 41 | foreach ($data as $key => $val) $this->$key = $val; 42 | } 43 | 44 | // Apply the filter to an incoming request 45 | public function filter(IncomingRequest $request) 46 | { 47 | return $this->passOnDemand($request) 48 | && $this->passSampling() 49 | && $this->passExcept($request) 50 | && $this->passOnly($request) 51 | && $this->passExceptPreflight($request) 52 | && $this->passCallback($request); 53 | } 54 | 55 | protected function passOnDemand(IncomingRequest $request) 56 | { 57 | if (! $this->onDemand) return true; 58 | 59 | if ($this->onDemand !== true) { 60 | $input = isset($request->input['clockwork-profile']) ? $request->input['clockwork-profile'] : ''; 61 | $cookie = isset($request->cookies['clockwork-profile']) ? $request->cookies['clockwork-profile'] : ''; 62 | 63 | return hash_equals($this->onDemand, $input) || hash_equals($this->onDemand, $cookie); 64 | } 65 | 66 | return isset($request->input['clockwork-profile']) || isset($request->cookies['clockwork-profile']); 67 | } 68 | 69 | protected function passSampling() 70 | { 71 | if (! $this->sample) return true; 72 | 73 | return mt_rand(0, $this->sample) == $this->sample; 74 | } 75 | 76 | protected function passExcept(IncomingRequest $request) 77 | { 78 | if (! count($this->except)) return true; 79 | 80 | foreach ($this->except as $pattern) { 81 | if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return false; 82 | } 83 | 84 | return true; 85 | } 86 | 87 | protected function passOnly(IncomingRequest $request) 88 | { 89 | if (! count($this->only)) return true; 90 | 91 | foreach ($this->only as $pattern) { 92 | if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return true; 93 | } 94 | 95 | return false; 96 | } 97 | 98 | protected function passExceptPreflight(IncomingRequest $request) 99 | { 100 | if (! $this->exceptPreflight) return true; 101 | 102 | return strtoupper($request->method) != 'OPTIONS'; 103 | } 104 | 105 | protected function passCallback(IncomingRequest $request) 106 | { 107 | if (! $this->callback) return true; 108 | 109 | return call_user_func($this->callback, $request); 110 | } 111 | 112 | public function __call($method, $parameters) 113 | { 114 | if (! count($parameters)) return $this->$method; 115 | 116 | $this->$method = count($parameters) ? $parameters[0] : true; 117 | 118 | return $this; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Clockwork/Support/Vanilla/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 26 | } 27 | 28 | // Returns a new middleware instance with a default singleton Clockwork instance, takes an additional configuration as argument 29 | public static function init($config = []) 30 | { 31 | return new static(Clockwork::init($config)); 32 | } 33 | 34 | // Sets a PSR-17 compatible response factory. When using the middleware with routing enabled, response factory must be manually set 35 | // or the php-http/discovery has to be intalled for zero-configuration use 36 | public function withResponseFactory(ResponseFactoryInterface $responseFactory) 37 | { 38 | $this->responseFactory = $responseFactory; 39 | return $this; 40 | } 41 | 42 | // Disables routing handling in the middleware. When disabled additional manual configuration of the application router is required. 43 | public function withoutRouting() 44 | { 45 | $this->handleRouting = false; 46 | return $this; 47 | } 48 | 49 | // Process the middleware 50 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) :ResponseInterface 51 | { 52 | $request = $request->withAttribute('clockwork', $this->clockwork); 53 | 54 | $this->clockwork->event('Controller')->begin(); 55 | 56 | if ($response = $this->handleApiRequest($request)) return $response; 57 | if ($response = $this->handleWebRequest($request)) return $response; 58 | 59 | $response = $handler->handle($request); 60 | 61 | return $this->clockwork->usePsrMessage($request, $response)->requestProcessed(); 62 | } 63 | 64 | // Handle a Clockwork REST api request if routing is enabled 65 | protected function handleApiRequest(ServerRequestInterface $request) 66 | { 67 | $path = $this->clockwork->getConfig()['api']; 68 | 69 | if (! $this->handleRouting) return; 70 | if (! preg_match("#{$path}.*#", $request->getUri()->getPath())) return; 71 | 72 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->handleMetadata(); 73 | } 74 | 75 | // Handle a Clockwork Web interface request if routing is enabled 76 | protected function handleWebRequest(ServerRequestInterface $request) 77 | { 78 | $path = is_string($this->clockwork->getConfig()['web']['enable']) ? $this->clockwork->getConfig()['web']['enable'] : '/clockwork'; 79 | 80 | if (! $this->handleRouting) return; 81 | if ($request->getUri()->getPath() != $path) return; 82 | 83 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->returnWeb(); 84 | } 85 | 86 | protected function prepareResponse() 87 | { 88 | if (! $this->responseFactory && ! class_exists(Psr17Factory::class)) { 89 | throw new \Exception('The Clockwork vanilla middleware requires a response factory or the php-http/discovery package to be installed.'); 90 | } 91 | 92 | return ($this->responseFactory ?: new Psr17Factory)->createResponse(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Clockwork/Request/Log.php: -------------------------------------------------------------------------------- 1 | messages = $messages; 17 | } 18 | 19 | // Log a new message, with a level and context, context can be used to override serializer defaults, 20 | // $context['trace'] = true can be used to force collecting a stack trace 21 | public function log($level = LogLevel::INFO, $message = null, array $context = []) 22 | { 23 | $trace = $this->hasTrace($context) ? $context['trace'] : StackTrace::get()->resolveViewName(); 24 | 25 | $this->messages[] = [ 26 | 'message' => (new Serializer($context))->normalize($message), 27 | 'exception' => $this->formatException($context), 28 | 'context' => $this->formatContext($context), 29 | 'level' => $level, 30 | 'time' => microtime(true), 31 | 'trace' => (new Serializer(! empty($context['trace']) ? [ 'traces' => true ] : []))->trace($trace) 32 | ]; 33 | } 34 | 35 | public function emergency($message, array $context = []) 36 | { 37 | $this->log(LogLevel::EMERGENCY, $message, $context); 38 | } 39 | 40 | public function alert($message, array $context = []) 41 | { 42 | $this->log(LogLevel::ALERT, $message, $context); 43 | } 44 | 45 | public function critical($message, array $context = []) 46 | { 47 | $this->log(LogLevel::CRITICAL, $message, $context); 48 | } 49 | 50 | public function error($message, array $context = []) 51 | { 52 | $this->log(LogLevel::ERROR, $message, $context); 53 | } 54 | 55 | public function warning($message, array $context = []) 56 | { 57 | $this->log(LogLevel::WARNING, $message, $context); 58 | } 59 | 60 | public function notice($message, array $context = []) 61 | { 62 | $this->log(LogLevel::NOTICE, $message, $context); 63 | } 64 | 65 | public function info($message, array $context = []) 66 | { 67 | $this->log(LogLevel::INFO, $message, $context); 68 | } 69 | 70 | public function debug($message, array $context = []) 71 | { 72 | $this->log(LogLevel::DEBUG, $message, $context); 73 | } 74 | 75 | // Merge another log instance into the current log 76 | public function merge(Log $log) 77 | { 78 | $this->messages = array_merge($this->messages, $log->messages); 79 | 80 | return $this; 81 | } 82 | 83 | // Sort the log messages by timestamp 84 | public function sort() 85 | { 86 | usort($this->messages, function ($a, $b) { return $a['time'] * 1000 - $b['time'] * 1000; }); 87 | } 88 | 89 | // Get all messages as an array 90 | public function toArray() 91 | { 92 | return $this->messages; 93 | } 94 | 95 | // Format message context, removes exception and trace if we are serializing them 96 | protected function formatContext($context) 97 | { 98 | if ($this->hasException($context)) unset($context['exception']); 99 | if ($this->hasTrace($context)) unset($context['trace']); 100 | 101 | return (new Serializer)->normalize($context); 102 | } 103 | 104 | // Format exception if present in the context 105 | protected function formatException($context) 106 | { 107 | if ($this->hasException($context)) { 108 | return (new Serializer)->exception($context['exception']); 109 | } 110 | } 111 | 112 | // Check if context has serializable trace 113 | protected function hasTrace($context) 114 | { 115 | return ! empty($context['trace']) && $context['trace'] instanceof StackTrace && empty($context['raw']); 116 | } 117 | 118 | // Check if context has serializable exception 119 | protected function hasException($context) 120 | { 121 | return ! empty($context['exception']) 122 | && ($context['exception'] instanceof \Throwable || $context['exception'] instanceof \Exception) 123 | && empty($context['raw']); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Clockwork/Support/Slim/Legacy/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | clockwork = $storagePathOrClockwork instanceof Clockwork 21 | ? $storagePathOrClockwork : $this->createDefaultClockwork($storagePathOrClockwork); 22 | $this->startTime = $startTime ?: microtime(true); 23 | } 24 | 25 | public function __invoke(Request $request, Response $response, callable $next) 26 | { 27 | return $this->process($request, $response, $next); 28 | } 29 | 30 | public function process(Request $request, Response $response, callable $next) 31 | { 32 | $authUri = '#/__clockwork/auth#'; 33 | if (preg_match($authUri, $request->getUri()->getPath(), $matches)) { 34 | return $this->authenticate($response, $request); 35 | } 36 | 37 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; 38 | if (preg_match($clockworkDataUri, $request->getUri()->getPath(), $matches)) { 39 | $matches = array_merge([ 'id' => null, 'direction' => null, 'count' => null ], $matches); 40 | return $this->retrieveRequest($response, $request, $matches['id'], $matches['direction'], $matches['count']); 41 | } 42 | 43 | $response = $next($request, $response); 44 | 45 | return $this->logRequest($request, $response); 46 | } 47 | 48 | protected function authenticate(Response $response, Request $request) 49 | { 50 | $token = $this->clockwork->authenticator()->attempt($request->getParsedBody()); 51 | 52 | return $response->withJson([ 'token' => $token ])->withStatus($token ? 200 : 403); 53 | } 54 | 55 | protected function retrieveRequest(Response $response, Request $request, $id, $direction, $count) 56 | { 57 | $authenticator = $this->clockwork->authenticator(); 58 | $storage = $this->clockwork->storage(); 59 | 60 | $authenticated = $authenticator->check(current($request->getHeader('X-Clockwork-Auth'))); 61 | 62 | if ($authenticated !== true) { 63 | return $response 64 | ->withJson([ 'message' => $authenticated, 'requires' => $authenticator->requires() ]) 65 | ->withStatus(403); 66 | } 67 | 68 | if ($direction == 'previous') { 69 | $data = $storage->previous($id, $count); 70 | } elseif ($direction == 'next') { 71 | $data = $storage->next($id, $count); 72 | } elseif ($id == 'latest') { 73 | $data = $storage->latest(); 74 | } else { 75 | $data = $storage->find($id); 76 | } 77 | 78 | return $response 79 | ->withHeader('Content-Type', 'application/json') 80 | ->withJson($data); 81 | } 82 | 83 | protected function logRequest(Request $request, Response $response) 84 | { 85 | $this->clockwork->timeline()->finalize($this->startTime); 86 | $this->clockwork->addDataSource(new PsrMessageDataSource($request, $response)); 87 | 88 | $this->clockwork->resolveRequest(); 89 | $this->clockwork->storeRequest(); 90 | 91 | $clockworkRequest = $this->clockwork->request(); 92 | 93 | $response = $response 94 | ->withHeader('X-Clockwork-Id', $clockworkRequest->id) 95 | ->withHeader('X-Clockwork-Version', Clockwork::VERSION); 96 | 97 | if ($basePath = $request->getUri()->getBasePath()) { 98 | $response = $response->withHeader('X-Clockwork-Path', $basePath); 99 | } 100 | 101 | return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value()); 102 | } 103 | 104 | protected function createDefaultClockwork($storagePath) 105 | { 106 | $clockwork = new Clockwork(); 107 | 108 | $clockwork->storage(new FileStorage($storagePath)); 109 | $clockwork->authenticator(new NullAuthenticator); 110 | 111 | return $clockwork; 112 | } 113 | } -------------------------------------------------------------------------------- /Clockwork/Helpers/StackTrace.php: -------------------------------------------------------------------------------- 1 | frames = $frames; 40 | $this->basePath = $basePath; 41 | $this->vendorPath = $vendorPath; 42 | } 43 | 44 | // Get all frames 45 | public function frames() 46 | { 47 | return $this->frames; 48 | } 49 | 50 | // Get the first frame, optionally filtered by a stack filter or a closure 51 | public function first($filter = null) 52 | { 53 | if (! $filter) return reset($this->frames); 54 | 55 | if ($filter instanceof StackFilter) $filter = $filter->closure(); 56 | 57 | foreach ($this->frames as $frame) { 58 | if ($filter($frame)) return $frame; 59 | } 60 | } 61 | 62 | // Get the last frame, optionally filtered by a stack filter or a closure 63 | public function last($filter = null) 64 | { 65 | if (! $filter) return $this->frames[count($this->frames) - 1]; 66 | 67 | if ($filter instanceof StackFilter) $filter = $filter->closure(); 68 | 69 | foreach (array_reverse($this->frames) as $frame) { 70 | if ($filter($frame)) return $frame; 71 | } 72 | } 73 | 74 | // Get trace filtered by a stack filter or a closure 75 | public function filter($filter = null) 76 | { 77 | if ($filter instanceof StackFilter) $filter = $filter->closure(); 78 | 79 | return $this->copy(array_values(array_filter($this->frames, $filter))); 80 | } 81 | 82 | // Get trace skipping a number of frames or frames matching a stack filter or a closure 83 | public function skip($count = null) 84 | { 85 | if ($count instanceof StackFilter) $count = $count->closure(); 86 | if ($count instanceof \Closure) $count = array_search($this->first($count), $this->frames); 87 | 88 | return $this->copy(array_slice($this->frames, $count)); 89 | } 90 | 91 | // Get trace with a number of frames from the top 92 | public function limit($count = null) 93 | { 94 | return $this->copy(array_slice($this->frames, 0, $count)); 95 | } 96 | 97 | // Get a copy of the trace 98 | public function copy($frames = null) 99 | { 100 | return new static($frames ?: $this->frames, $this->basePath, $this->vendorPath); 101 | } 102 | 103 | protected static function resolveBasePath() 104 | { 105 | return substr(__DIR__, 0, strpos(__DIR__, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)); 106 | } 107 | 108 | protected static function resolveVendorPath() 109 | { 110 | return static::resolveBasePath() . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR; 111 | } 112 | 113 | // Fixes call_user_func stack frames missing file and line 114 | protected static function fixCallUserFuncFrame($frame, array $trace, $index) 115 | { 116 | if (isset($frame['file'])) return $frame; 117 | 118 | $nextFrame = isset($trace[$index + 1]) ? $trace[$index + 1] : null; 119 | 120 | if (! $nextFrame || ! in_array($nextFrame['function'], [ 'call_user_func', 'call_user_func_array' ])) return $frame; 121 | 122 | $frame['file'] = $nextFrame['file']; 123 | $frame['line'] = $nextFrame['line']; 124 | 125 | return $frame; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Clockwork/Support/Slim/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | app = $app; 22 | $this->clockwork = $storagePathOrClockwork instanceof Clockwork 23 | ? $storagePathOrClockwork : $this->createDefaultClockwork($storagePathOrClockwork); 24 | $this->startTime = $startTime ?: microtime(true); 25 | } 26 | 27 | public function __invoke(Request $request, RequestHandler $handler) 28 | { 29 | return $this->process($request, $handler); 30 | } 31 | 32 | public function process(Request $request, RequestHandler $handler) 33 | { 34 | $authUri = '#/__clockwork/auth#'; 35 | if (preg_match($authUri, $request->getUri()->getPath(), $matches)) { 36 | return $this->authenticate($request); 37 | } 38 | 39 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; 40 | if (preg_match($clockworkDataUri, $request->getUri()->getPath(), $matches)) { 41 | $matches = array_merge([ 'id' => null, 'direction' => null, 'count' => null ], $matches); 42 | return $this->retrieveRequest($request, $matches['id'], $matches['direction'], $matches['count']); 43 | } 44 | 45 | $response = $handler->handle($request); 46 | 47 | return $this->logRequest($request, $response); 48 | } 49 | 50 | protected function authenticate(Request $request) 51 | { 52 | $token = $this->clockwork->authenticator()->attempt($request->getParsedBody()); 53 | 54 | return $this->jsonResponse([ 'token' => $token ], $token ? 200 : 403); 55 | } 56 | 57 | protected function retrieveRequest(Request $request, $id, $direction, $count) 58 | { 59 | $authenticator = $this->clockwork->authenticator(); 60 | $storage = $this->clockwork->storage(); 61 | 62 | $authenticated = $authenticator->check(current($request->getHeader('X-Clockwork-Auth'))); 63 | 64 | if ($authenticated !== true) { 65 | return $this->jsonResponse([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403); 66 | } 67 | 68 | if ($direction == 'previous') { 69 | $data = $storage->previous($id, $count); 70 | } elseif ($direction == 'next') { 71 | $data = $storage->next($id, $count); 72 | } elseif ($id == 'latest') { 73 | $data = $storage->latest(); 74 | } else { 75 | $data = $storage->find($id); 76 | } 77 | 78 | return $this->jsonResponse($data); 79 | } 80 | 81 | protected function logRequest(Request $request, $response) 82 | { 83 | $this->clockwork->timeline()->finalize($this->startTime); 84 | $this->clockwork->addDataSource(new PsrMessageDataSource($request, $response)); 85 | 86 | $this->clockwork->resolveRequest(); 87 | $this->clockwork->storeRequest(); 88 | 89 | $clockworkRequest = $this->clockwork->request(); 90 | 91 | $response = $response 92 | ->withHeader('X-Clockwork-Id', $clockworkRequest->id) 93 | ->withHeader('X-Clockwork-Version', Clockwork::VERSION); 94 | 95 | if ($basePath = $this->app->getBasePath()) { 96 | $response = $response->withHeader('X-Clockwork-Path', "$basePath/__clockwork/"); 97 | } 98 | 99 | return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value()); 100 | } 101 | 102 | protected function createDefaultClockwork($storagePath) 103 | { 104 | $clockwork = new Clockwork(); 105 | 106 | $clockwork->storage(new FileStorage($storagePath)); 107 | $clockwork->authenticator(new NullAuthenticator); 108 | 109 | return $clockwork; 110 | } 111 | 112 | protected function jsonResponse($data, $status = 200) 113 | { 114 | $response = $this->app->getResponseFactory() 115 | ->createResponse($status) 116 | ->withHeader('Content-Type', 'application/json'); 117 | 118 | $response->getBody()->write(json_encode($data)); 119 | 120 | return $response; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Clockwork/Helpers/StackFilter.php: -------------------------------------------------------------------------------- 1 | classes = array_merge($this->classes, is_array($classes) ? $classes : [ $classes ]); 29 | return $this; 30 | } 31 | 32 | public function isNotClass($classes) 33 | { 34 | $this->notClasses = array_merge($this->notClasses, is_array($classes) ? $classes : [ $classes ]); 35 | return $this; 36 | } 37 | 38 | public function isFile($files) 39 | { 40 | $this->files = array_merge($this->files, is_array($files) ? $files : [ $files ]); 41 | return $this; 42 | } 43 | 44 | public function isNotFile($files) 45 | { 46 | $this->notFiles = array_merge($this->notFiles, is_array($files) ? $files : [ $files ]); 47 | return $this; 48 | } 49 | 50 | public function isFunction($functions) 51 | { 52 | $this->functions = array_merge($this->functions, is_array($functions) ? $functions : [ $functions ]); 53 | return $this; 54 | } 55 | 56 | public function isNotFunction($functions) 57 | { 58 | $this->notFunctions = array_merge($this->notFunctions, is_array($functions) ? $functions : [ $functions ]); 59 | return $this; 60 | } 61 | 62 | public function isNamespace($namespaces) 63 | { 64 | $this->namespaces = array_merge($this->namespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]); 65 | return $this; 66 | } 67 | 68 | public function isNotNamespace($namespaces) 69 | { 70 | $this->notNamespaces = array_merge($this->notNamespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]); 71 | return $this; 72 | } 73 | 74 | public function isVendor($vendors) 75 | { 76 | $this->vendors = array_merge($this->vendors, is_array($vendors) ? $vendors : [ $vendors ]); 77 | return $this; 78 | } 79 | 80 | public function isNotVendor($vendors) 81 | { 82 | $this->notVendors = array_merge($this->notVendors, is_array($vendors) ? $vendors : [ $vendors ]); 83 | return $this; 84 | } 85 | 86 | // Apply the filter to a stack frame 87 | public function filter(StackFrame $frame) 88 | { 89 | return $this->matchesClass($frame) 90 | && $this->matchesFile($frame) 91 | && $this->matchesFunction($frame) 92 | && $this->matchesNamespace($frame) 93 | && $this->matchesVendor($frame); 94 | } 95 | 96 | // Return a closure calling this filter 97 | public function closure() 98 | { 99 | return function ($frame) { return $this->filter($frame); }; 100 | } 101 | 102 | protected function matchesClass(StackFrame $frame) 103 | { 104 | if (count($this->classes) && ! in_array($frame->class, $this->classes)) return false; 105 | if (count($this->notClasses) && in_array($frame->class, $this->notClasses)) return false; 106 | 107 | return true; 108 | } 109 | 110 | protected function matchesFile(StackFrame $frame) 111 | { 112 | if (count($this->files) && ! in_array($frame->file, $this->files)) return false; 113 | if (count($this->notFiles) && in_array($frame->file, $this->notFiles)) return false; 114 | 115 | return true; 116 | } 117 | 118 | protected function matchesFunction(StackFrame $frame) 119 | { 120 | if (count($this->functions) && ! in_array($frame->function, $this->functions)) return false; 121 | if (count($this->notFunctions) && in_array($frame->function, $this->notFunctions)) return false; 122 | 123 | return true; 124 | } 125 | 126 | protected function matchesNamespace(StackFrame $frame) 127 | { 128 | foreach ($this->notNamespaces as $namespace) { 129 | if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return false; 130 | } 131 | 132 | if (! count($this->namespaces)) return true; 133 | 134 | foreach ($this->namespaces as $namespace) { 135 | if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return true; 136 | } 137 | 138 | return false; 139 | } 140 | 141 | protected function matchesVendor(StackFrame $frame) 142 | { 143 | if (count($this->vendors) && ! in_array($frame->vendor, $this->vendors)) return false; 144 | if (count($this->notVendors) && in_array($frame->vendor, $this->notVendors)) return false; 145 | 146 | return true; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelEventsDataSource.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 25 | 26 | $this->ignoredEvents = is_array($ignoredEvents) 27 | ? array_merge($ignoredEvents, $this->defaultIgnoredEvents()) : []; 28 | } 29 | 30 | // Adds fired events to the request 31 | public function resolve(Request $request) 32 | { 33 | $request->events = array_merge($request->events, $this->events); 34 | 35 | return $request; 36 | } 37 | 38 | // Reset the data source to an empty state, clearing any collected data 39 | public function reset() 40 | { 41 | $this->events = []; 42 | } 43 | 44 | // Start listening to the events 45 | public function listenToEvents() 46 | { 47 | $this->dispatcher->listen('*', function ($event = null, $data = null) { 48 | if (method_exists($this->dispatcher, 'firing')) { // Laravel 5.0 - 5.3 49 | $data = func_get_args(); 50 | $event = $this->dispatcher->firing(); 51 | } 52 | 53 | $this->registerEvent($event, $data); 54 | }); 55 | } 56 | 57 | // Collect a fired event, prepares data for serialization and resolves registered listeners 58 | protected function registerEvent($event, array $data) 59 | { 60 | if (! $this->shouldCollect($event)) return; 61 | 62 | $trace = StackTrace::get()->resolveViewName(); 63 | 64 | $event = [ 65 | 'event' => $event, 66 | 'data' => (new Serializer)->normalize(count($data) == 1 && isset($data[0]) ? $data[0] : $data), 67 | 'time' => microtime(true), 68 | 'listeners' => $this->findListenersFor($event), 69 | 'trace' => (new Serializer)->trace($trace) 70 | ]; 71 | 72 | if ($this->passesFilters([ $event ])) { 73 | $this->events[] = $event; 74 | } 75 | } 76 | 77 | // Returns registered listeners for the specified event 78 | protected function findListenersFor($event) 79 | { 80 | $listener = $this->dispatcher->getListeners($event)[0]; 81 | 82 | return array_filter(array_map(function ($listener) { 83 | if ($listener instanceof \Closure) { 84 | // Laravel 5.4+ (and earlier versions in some cases) wrap the listener into a closure, 85 | // attempt to resolve the original listener 86 | $use = (new \ReflectionFunction($listener))->getStaticVariables(); 87 | $listener = isset($use['listener']) ? $use['listener'] : $listener; 88 | } 89 | 90 | if (is_string($listener)) { 91 | return $listener; 92 | } elseif (is_array($listener) && count($listener) == 2) { 93 | if (is_object($listener[0])) { 94 | return get_class($listener[0]) . '@' . $listener[1]; 95 | } else { 96 | return $listener[0] . '::' . $listener[1]; 97 | } 98 | } elseif ($listener instanceof \Closure) { 99 | $listener = new \ReflectionFunction($listener); 100 | 101 | if (strpos($listener->getNamespaceName(), 'Clockwork\\') === 0) { // skip our own listeners 102 | return; 103 | } 104 | 105 | $filename = str_replace(base_path(), '', $listener->getFileName()); 106 | $startLine = $listener->getStartLine(); 107 | $endLine = $listener->getEndLine(); 108 | 109 | return "Closure ({$filename}:{$startLine}-{$endLine})"; 110 | } 111 | }, $this->dispatcher->getListeners($event))); 112 | } 113 | 114 | // Returns whether the event should be collected (depending on ignored events) 115 | protected function shouldCollect($event) 116 | { 117 | return ! preg_match('/^(?:' . implode('|', $this->ignoredEvents) . ')$/', $event); 118 | } 119 | 120 | // Returns default ignored events (framework-specific events) 121 | protected function defaultIgnoredEvents() 122 | { 123 | return [ 124 | 'Illuminate\\\\.+', 125 | 'Laravel\\\\.+', 126 | 'auth\.(?:attempt|login|logout)', 127 | 'artisan\.start', 128 | 'bootstrapped:.+', 129 | 'composing:.+', 130 | 'creating:.+', 131 | 'illuminate\.query', 132 | 'connection\..+', 133 | 'eloquent\..+', 134 | 'kernel\.handled', 135 | 'illuminate\.log', 136 | 'mailer\.sending', 137 | 'router\.(?:before|after|matched)', 138 | 'router.filter:.+', 139 | 'locale\.changed', 140 | 'clockwork\..+' 141 | ]; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelCacheDataSource.php: -------------------------------------------------------------------------------- 1 | 0, 'hit' => 0, 'write' => 0, 'delete' => 0 21 | ]; 22 | 23 | // Whether we are collecting cache queries or stats only 24 | protected $collectQueries = true; 25 | 26 | // Whether we are collecting values from cache queries 27 | protected $collectValues = true; 28 | 29 | // Create a new data source instance, takes an event dispatcher and additional options as argument 30 | public function __construct(EventDispatcher $eventDispatcher, $collectQueries = true, $collectValues = true) 31 | { 32 | $this->eventDispatcher = $eventDispatcher; 33 | 34 | $this->collectQueries = $collectQueries; 35 | $this->collectValues = $collectValues; 36 | } 37 | 38 | // Adds cache queries and stats to the request 39 | public function resolve(Request $request) 40 | { 41 | $request->cacheQueries = array_merge($request->cacheQueries, $this->queries); 42 | 43 | $request->cacheReads += $this->count['read']; 44 | $request->cacheHits += $this->count['hit']; 45 | $request->cacheWrites += $this->count['write']; 46 | $request->cacheDeletes += $this->count['delete']; 47 | 48 | return $request; 49 | } 50 | 51 | // Reset the data source to an empty state, clearing any collected data 52 | public function reset() 53 | { 54 | $this->queries = []; 55 | 56 | $this->count = [ 57 | 'read' => 0, 'hit' => 0, 'write' => 0, 'delete' => 0 58 | ]; 59 | } 60 | 61 | // Start listening to cache events 62 | public function listenToEvents() 63 | { 64 | if (class_exists(\Illuminate\Cache\Events\CacheHit::class)) { 65 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheHit::class, function ($event) { 66 | $this->registerQuery([ 'type' => 'hit', 'key' => $event->key, 'value' => $event->value ]); 67 | }); 68 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheMissed::class, function ($event) { 69 | $this->registerQuery([ 'type' => 'miss', 'key' => $event->key ]); 70 | }); 71 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyWritten::class, function ($event) { 72 | $this->registerQuery([ 73 | 'type' => 'write', 'key' => $event->key, 'value' => $event->value, 74 | 'expiration' => property_exists($event, 'seconds') ? $event->seconds : $event->minutes * 60 75 | ]); 76 | }); 77 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyForgotten::class, function ($event) { 78 | $this->registerQuery([ 'type' => 'delete', 'key' => $event->key ]); 79 | }); 80 | } else { 81 | // legacy Laravel 5.1 style events 82 | $this->eventDispatcher->listen('cache.hit', function ($key, $value) { 83 | $this->registerQuery([ 'type' => 'hit', 'key' => $key, 'value' => $value ]); 84 | }); 85 | $this->eventDispatcher->listen('cache.missed', function ($key) { 86 | $this->registerQuery([ 'type' => 'miss', 'key' => $key ]); 87 | }); 88 | $this->eventDispatcher->listen('cache.write', function ($key, $value, $minutes) { 89 | $this->registerQuery([ 90 | 'type' => 'write', 'key' => $key, 'value' => $value, 'expiration' => $minutes * 60 91 | ]); 92 | }); 93 | $this->eventDispatcher->listen('cache.delete', function ($key) { 94 | $this->registerQuery([ 'type' => 'delete', 'key' => $key ]); 95 | }); 96 | } 97 | } 98 | 99 | // Collect an executed query 100 | protected function registerQuery(array $query) 101 | { 102 | $trace = StackTrace::get()->resolveViewName(); 103 | 104 | $record = [ 105 | 'type' => $query['type'], 106 | 'key' => $query['key'], 107 | 'expiration' => isset($query['expiration']) ? $query['expiration'] : null, 108 | 'time' => microtime(true), 109 | 'connection' => null, 110 | 'trace' => (new Serializer)->trace($trace) 111 | ]; 112 | 113 | if ($this->collectValues && isset($query['value'])) { 114 | $record['value'] = (new Serializer)->normalize($query['value']); 115 | } 116 | 117 | $this->incrementQueryCount($record); 118 | 119 | if ($this->collectQueries && $this->passesFilters([ $record ])) { 120 | $this->queries[] = $record; 121 | } 122 | } 123 | 124 | // Increment query counts for collected query 125 | protected function incrementQueryCount($query) 126 | { 127 | if ($query['type'] == 'write') { 128 | $this->count['write']++; 129 | } elseif ($query['type'] == 'delete') { 130 | $this->count['delete']++; 131 | } else { 132 | $this->count['read']++; 133 | 134 | if ($query['type'] == 'hit') { 135 | $this->count['hit']++; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Clockwork/DataSource/PhpDataSource.php: -------------------------------------------------------------------------------- 1 | time = PHP_SAPI !== 'cli' ? $this->getRequestTime() : $request->time; 14 | $request->method = $this->getRequestMethod(); 15 | $request->url = $this->getRequestUrl(); 16 | $request->uri = $this->getRequestUri(); 17 | $request->headers = $this->getRequestHeaders(); 18 | $request->getData = $this->getGetData(); 19 | $request->postData = $this->getPostData(); 20 | $request->requestData = $this->getRequestData(); 21 | $request->sessionData = $this->getSessionData(); 22 | $request->cookies = $this->getCookies(); 23 | $request->responseStatus = $this->getResponseStatus(); 24 | $request->responseTime = $this->getResponseTime(); 25 | $request->memoryUsage = $this->getMemoryUsage(); 26 | 27 | return $request; 28 | } 29 | 30 | // Get the request cookies (normalized with passwords removed) 31 | protected function getCookies() 32 | { 33 | return $this->removePasswords((new Serializer)->normalizeEach($_COOKIE)); 34 | } 35 | 36 | // Get the request GET data (normalized with passwords removed) 37 | protected function getGetData() 38 | { 39 | return $this->removePasswords((new Serializer)->normalizeEach($_GET)); 40 | } 41 | 42 | // Get the request POST data (normalized with passwords removed) 43 | protected function getPostData() 44 | { 45 | return $this->removePasswords((new Serializer)->normalizeEach($_POST)); 46 | } 47 | 48 | // Get the request body data (attempt to parse as json, normalized with passwords removed) 49 | protected function getRequestData() 50 | { 51 | // The data will already be parsed into POST data by PHP in case of application/x-www-form-urlencoded requests 52 | if (count($_POST)) return; 53 | 54 | $requestData = file_get_contents('php://input'); 55 | $requestJsonData = json_decode($requestData, true); 56 | 57 | return is_array($requestJsonData) 58 | ? $this->removePasswords((new Serializer)->normalizeEach($requestJsonData)) 59 | : $requestData; 60 | } 61 | 62 | // Get the request headers 63 | protected function getRequestHeaders() 64 | { 65 | $headers = []; 66 | 67 | foreach ($_SERVER as $key => $value) { 68 | if (substr($key, 0, 5) !== 'HTTP_') continue; 69 | 70 | $header = substr($key, 5); 71 | $header = str_replace('_', ' ', $header); 72 | $header = ucwords(strtolower($header)); 73 | $header = str_replace(' ', '-', $header); 74 | 75 | if (! isset($headers[$header])) { 76 | $headers[$header] = [ $value ]; 77 | } else { 78 | $headers[$header][] = $value; 79 | } 80 | } 81 | 82 | ksort($headers); 83 | 84 | return $headers; 85 | } 86 | 87 | // Get the request method 88 | protected function getRequestMethod() 89 | { 90 | if (isset($_SERVER['REQUEST_METHOD'])) { 91 | return $_SERVER['REQUEST_METHOD']; 92 | } 93 | } 94 | 95 | // Get the response time 96 | protected function getRequestTime() 97 | { 98 | if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { 99 | return $_SERVER['REQUEST_TIME_FLOAT']; 100 | } 101 | } 102 | 103 | // Get the request URL 104 | protected function getRequestUrl() 105 | { 106 | $https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'; 107 | $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null; 108 | $addr = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : null; 109 | $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : null; 110 | $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null; 111 | 112 | $scheme = $https ? 'https' : 'http'; 113 | $host = $host ?: $addr; 114 | $port = (! $https && $port != 80 || $https && $port != 443) ? ":{$port}" : ''; 115 | 116 | // remove port number from the host 117 | $host = $host ? preg_replace('/:\d+$/', '', trim($host)) : null; 118 | 119 | return "{$scheme}://{$host}{$port}{$uri}"; 120 | } 121 | 122 | // Get the request URI 123 | protected function getRequestUri() 124 | { 125 | if (isset($_SERVER['REQUEST_URI'])) { 126 | return $_SERVER['REQUEST_URI']; 127 | } 128 | } 129 | 130 | // Get the response status code 131 | protected function getResponseStatus() 132 | { 133 | return http_response_code(); 134 | } 135 | 136 | // Get the response time (current time, assuming most of the application code has already run at this point) 137 | protected function getResponseTime() 138 | { 139 | return microtime(true); 140 | } 141 | 142 | // Get the session data (normalized with passwords removed) 143 | protected function getSessionData() 144 | { 145 | if (! isset($_SESSION)) return []; 146 | 147 | return $this->removePasswords((new Serializer)->normalizeEach($_SESSION)); 148 | } 149 | 150 | // Get the peak memory usage in bytes 151 | protected function getMemoryUsage() 152 | { 153 | return memory_get_peak_usage(true); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Clockwork/Helpers/Serializer.php: -------------------------------------------------------------------------------- 1 | [ 15 | \Illuminate\Container\Container::class, 16 | \Illuminate\Foundation\Application::class, 17 | \Laravel\Lumen\Application::class 18 | ], 19 | 'limit' => 10, 20 | 'toArray' => false, 21 | 'toString' => false, 22 | 'debugInfo' => true, 23 | 'jsonSerialize' => false, 24 | 'traces' => true, 25 | 'tracesFilter' => null, 26 | 'tracesSkip' => null, 27 | 'tracesLimit' => null 28 | ]; 29 | 30 | // Create a new instance optionally with options overriding defaults 31 | public function __construct(array $options = []) 32 | { 33 | $this->options = $options + static::$defaults; 34 | } 35 | 36 | // Set default options for all new instances 37 | public static function defaults(array $defaults) 38 | { 39 | static::$defaults = $defaults + static::$defaults; 40 | } 41 | 42 | // Prepares the passed data to be ready for serialization, takes any kind of data to normalize as the first 43 | // argument, other arguments are used internally in recursion 44 | public function normalize($data, $context = null, $limit = null) 45 | { 46 | if ($context === null) $context = [ 'references' => [] ]; 47 | if ($limit === null) $limit = $this->options['limit']; 48 | 49 | if (is_array($data)) { 50 | if ($limit === 0) return [ '__type__' => 'array', '__omitted__' => 'limit' ]; 51 | 52 | return [ '__type__' => 'array' ] + $this->normalizeEach($data, $context, $limit - 1); 53 | } elseif (is_object($data)) { 54 | if ($data instanceof \Closure) return [ '__type__' => 'anonymous function' ]; 55 | 56 | $className = get_class($data); 57 | $objectHash = spl_object_hash($data); 58 | 59 | if ($className === '__PHP_Incomplete_Class') return [ '__class__' => $className ]; 60 | 61 | if ($limit === 0) return [ '__class__' => $className, '__omitted__' => 'limit' ]; 62 | 63 | if (isset($context['references'][$objectHash])) return [ '__type__' => 'recursion' ]; 64 | 65 | $context['references'][$objectHash] = true; 66 | 67 | if (isset($this->cache[$objectHash])) return $this->cache[$objectHash]; 68 | 69 | if ($this->options['blackbox'] && in_array($className, $this->options['blackbox'])) { 70 | return $this->cache[$objectHash] = [ '__class__' => $className, '__omitted__' => 'blackbox' ]; 71 | } elseif ($this->options['toString'] && method_exists($data, '__toString')) { 72 | return $this->cache[$objectHash] = (string) $data; 73 | } 74 | 75 | if ($this->options['debugInfo'] && method_exists($data, '__debugInfo')) { 76 | $data = (array) $data->__debugInfo(); 77 | } elseif ($this->options['jsonSerialize'] && method_exists($data, 'jsonSerialize')) { 78 | $data = (array) $data->jsonSerialize(); 79 | } elseif ($this->options['toArray'] && method_exists($data, 'toArray')) { 80 | $data = (array) $data->toArray(); 81 | } else { 82 | $data = (array) $data; 83 | } 84 | 85 | $data = array_combine( 86 | array_map(function ($key) { 87 | // replace null-byte prefixes of protected and private properties used by php with * (protected) 88 | // and ~ (private) 89 | return preg_replace('/^\0.+?\0/', '~', str_replace("\0*\0", '*', $key)); 90 | }, array_keys($data)), 91 | $this->normalizeEach($data, $context, $limit - 1) 92 | ); 93 | 94 | return $this->cache[$objectHash] = [ '__class__' => $className ] + $data; 95 | } elseif (is_resource($data)) { 96 | return [ '__type__' => 'resource' ]; 97 | } 98 | 99 | return $data; 100 | } 101 | 102 | // Normalize each member of an array (doesn't add metadata for top level) 103 | public function normalizeEach($data, $context = null, $limit = null) { 104 | return array_map(function ($item) use ($context, $limit) { 105 | return $this->normalize($item, $context, $limit); 106 | }, $data); 107 | } 108 | 109 | // Normalize a stack trace instance 110 | public function trace(StackTrace $trace) 111 | { 112 | if (! $this->options['traces']) return null; 113 | 114 | if ($this->options['tracesFilter']) $trace = $trace->filter($this->options['tracesFilter']); 115 | if ($this->options['tracesSkip']) $trace = $trace->skip($this->options['tracesSkip']); 116 | if ($this->options['tracesLimit']) $trace = $trace->limit($this->options['tracesLimit']); 117 | 118 | return array_map(function ($frame) { 119 | return [ 120 | 'call' => $frame->call, 121 | 'file' => $frame->file, 122 | 'line' => $frame->line, 123 | 'isVendor' => (bool) $frame->vendor 124 | ]; 125 | }, $trace->frames()); 126 | } 127 | 128 | // Normalize an exception instance 129 | public function exception(/* Throwable */ $exception) 130 | { 131 | return [ 132 | 'type' => get_class($exception), 133 | 'message' => $exception->getMessage(), 134 | 'code' => $exception->getCode(), 135 | 'file' => $exception->getFile(), 136 | 'line' => $exception->getLine(), 137 | 'trace' => (new Serializer([ 'tracesLimit' => false ]))->trace(StackTrace::from($exception->getTrace())), 138 | 'previous' => $exception->getPrevious() ? $this->exception($exception->getPrevious()) : null 139 | ]; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelHttpClientDataSource.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 32 | 33 | $this->collectContent = $collectContent; 34 | $this->collectRawContent = $collectRawContent; 35 | } 36 | 37 | // Add sent notifications to the request 38 | public function resolve(Request $request) 39 | { 40 | $request->httpRequests = array_merge($request->httpRequests, $this->requests); 41 | 42 | return $request; 43 | } 44 | 45 | // Reset the data source to an empty state, clearing any collected data 46 | public function reset() 47 | { 48 | $this->requests = []; 49 | $this->executingRequests = []; 50 | } 51 | 52 | // Listen to the email and notification events 53 | public function listenToEvents() 54 | { 55 | $this->dispatcher->listen(ConnectionFailed::class, function ($event) { $this->connectionFailed($event); }); 56 | $this->dispatcher->listen(RequestSending::class, function ($event) { $this->sendingRequest($event); }); 57 | $this->dispatcher->listen(ResponseReceived::class, function ($event) { $this->responseReceived($event); }); 58 | } 59 | 60 | // Collect an executing request 61 | protected function sendingRequest(RequestSending $event) 62 | { 63 | $trace = StackTrace::get()->resolveViewName(); 64 | 65 | $request = (object) [ 66 | 'request' => (object) [ 67 | 'method' => $event->request->method(), 68 | 'url' => $this->removeAuthFromUrl($event->request->url()), 69 | 'headers' => $event->request->headers(), 70 | 'content' => $this->collectContent ? $event->request->data() : null, 71 | 'body' => $this->collectRawContent ? $event->request->body() : null 72 | ], 73 | 'response' => null, 74 | 'stats' => null, 75 | 'error' => null, 76 | 'time' => microtime(true), 77 | 'trace' => (new Serializer)->trace($trace) 78 | ]; 79 | 80 | if ($this->passesFilters([ $request ])) { 81 | $this->requests[] = $this->executingRequests[spl_object_hash($event->request)] = $request; 82 | } 83 | } 84 | 85 | // Update last request with response details and time taken 86 | protected function responseReceived($event) 87 | { 88 | if (! isset($this->executingRequests[spl_object_hash($event->request)])) return; 89 | 90 | $request = $this->executingRequests[spl_object_hash($event->request)]; 91 | $stats = $event->response->handlerStats(); 92 | 93 | $request->duration = (microtime(true) - $request->time) * 1000; 94 | $request->response = (object) [ 95 | 'status' => $event->response->status(), 96 | 'headers' => $event->response->headers(), 97 | 'content' => $this->collectContent ? $event->response->json() : null, 98 | 'body' => $this->collectRawContent ? $event->response->body() : null 99 | ]; 100 | $request->stats = (object) [ 101 | 'timing' => isset($stats['total_time_us']) ? (object) [ 102 | 'lookup' => $stats['namelookup_time_us'] / 1000, 103 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000, 104 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000, 105 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000 106 | ] : null, 107 | 'size' => (object) [ 108 | 'upload' => isset($stats['size_upload']) ? $stats['size_upload'] : null, 109 | 'download' => isset($stats['size_download']) ? $stats['size_download'] : null 110 | ], 111 | 'speed' => (object) [ 112 | 'upload' => isset($stats['speed_upload']) ? $stats['speed_upload'] : null, 113 | 'download' => isset($stats['speed_download']) ? $stats['speed_download'] : null 114 | ], 115 | 'hosts' => (object) [ 116 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null, 117 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null 118 | ], 119 | 'version' => isset($stats['http_version']) ? $stats['http_version'] : null 120 | ]; 121 | 122 | unset($this->executingRequests[spl_object_hash($event->request)]); 123 | } 124 | 125 | // Update last request with error when connection fails 126 | protected function connectionFailed($event) 127 | { 128 | if (! isset($this->executingRequests[spl_object_hash($event->request)])) return; 129 | 130 | $request = $this->executingRequests[spl_object_hash($event->request)]; 131 | 132 | $request->duration = (microtime(true) - $request->time) * 1000; 133 | $request->error = 'connection-failed'; 134 | 135 | unset($this->executingRequests[spl_object_hash($event->request)]); 136 | } 137 | 138 | // Removes username and password from the URL 139 | protected function removeAuthFromUrl($url) 140 | { 141 | return preg_replace('#^(.+?://)(.+?@)(.*)$#', '$1$3', $url); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Clockwork/Storage/Search.php: -------------------------------------------------------------------------------- 1 | $condition = isset($search[$condition]) ? $search[$condition] : []; 27 | } 28 | 29 | foreach ([ 'stopOnFirstMismatch' ] as $option) { 30 | $this->$option = isset($options[$option]) ? $options[$option] : $this->$option; 31 | } 32 | 33 | $this->method = array_map('strtolower', $this->method); 34 | } 35 | 36 | // Create a new instance from request input 37 | public static function fromRequest($data = []) 38 | { 39 | return new static($data); 40 | } 41 | 42 | // Check whether the request matches current search parameters 43 | public function matches(Request $request) 44 | { 45 | if ($request->type == RequestType::COMMAND) { 46 | return $this->matchesCommand($request); 47 | } elseif ($request->type == RequestType::QUEUE_JOB) { 48 | return $this->matchesQueueJob($request); 49 | } elseif ($request->type == RequestType::TEST) { 50 | return $this->matchesTest($request); 51 | } else { 52 | return $this->matchesRequest($request); 53 | } 54 | } 55 | 56 | // Check whether a request type request matches 57 | protected function matchesRequest(Request $request) 58 | { 59 | return $this->matchesString($this->type, RequestType::REQUEST) 60 | && $this->matchesString($this->uri, $request->uri) 61 | && $this->matchesString($this->controller, $request->controller) 62 | && $this->matchesExact($this->method, strtolower($request->method)) 63 | && $this->matchesNumber($this->status, $request->responseStatus) 64 | && $this->matchesNumber($this->time, $request->responseDuration) 65 | && $this->matchesDate($this->received, $request->time); 66 | } 67 | 68 | // Check whether a command type request matches 69 | protected function matchesCommand(Request $request) 70 | { 71 | return $this->matchesString($this->type, RequestType::COMMAND) 72 | && $this->matchesString($this->name, $request->commandName) 73 | && $this->matchesNumber($this->status, $request->commandExitCode) 74 | && $this->matchesNumber($this->time, $request->responseDuration) 75 | && $this->matchesDate($this->received, $request->time); 76 | } 77 | 78 | // Check whether a queue-job type request matches 79 | protected function matchesQueueJob(Request $request) 80 | { 81 | return $this->matchesString($this->type, RequestType::QUEUE_JOB) 82 | && $this->matchesString($this->name, $request->jobName) 83 | && $this->matchesString($this->status, $request->jobStatus) 84 | && $this->matchesNumber($this->time, $request->responseDuration) 85 | && $this->matchesDate($this->received, $request->time); 86 | } 87 | 88 | // Check whether a test type request matches 89 | protected function matchesTest(Request $request) 90 | { 91 | return $this->matchesString($this->type, RequestType::TEST) 92 | && $this->matchesString($this->name, $request->testName) 93 | && $this->matchesString($this->status, $request->testStatus) 94 | && $this->matchesNumber($this->time, $request->responseDuration) 95 | && $this->matchesDate($this->received, $request->time); 96 | } 97 | 98 | // Check if there are no search parameters specified 99 | public function isEmpty() 100 | { 101 | return ! count($this->uri) && ! count($this->controller) && ! count($this->method) && ! count($this->status) 102 | && ! count($this->time) && ! count($this->received) && ! count($this->name) && ! count($this->type); 103 | } 104 | 105 | // Check if there are some search parameters specified 106 | public function isNotEmpty() 107 | { 108 | return ! $this->isEmpty(); 109 | } 110 | 111 | // Check if the value matches date type search parameter 112 | protected function matchesDate($inputs, $value) 113 | { 114 | if (! count($inputs)) return true; 115 | 116 | foreach ($inputs as $input) { 117 | if (preg_match('/^<(.+)$/', $input, $match)) { 118 | if ($value < strtotime($match[1])) return true; 119 | } elseif (preg_match('/^>(.+)$/', $input, $match)) { 120 | if ($value > strtotime($match[1])) return true; 121 | } 122 | } 123 | 124 | return false; 125 | } 126 | 127 | // Check if the value matches exact type search parameter 128 | protected function matchesExact($inputs, $value) 129 | { 130 | if (! count($inputs)) return true; 131 | 132 | return in_array($value, $inputs); 133 | } 134 | 135 | // Check if the value matches number type search parameter 136 | protected function matchesNumber($inputs, $value) 137 | { 138 | if (! count($inputs)) return true; 139 | 140 | foreach ($inputs as $input) { 141 | if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) { 142 | if ($value < $match[1]) return true; 143 | } elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) { 144 | if ($value > $match[1]) return true; 145 | } elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) { 146 | if ($match[1] < $value && $value < $match[2]) return true; 147 | } else { 148 | if ($value == $input) return true; 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | 155 | // Check if the value matches string type search parameter 156 | protected function matchesString($inputs, $value) 157 | { 158 | if (! count($inputs)) return true; 159 | 160 | foreach ($inputs as $input) { 161 | if (strpos($value, $input) !== false) return true; 162 | } 163 | 164 | return false; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Clockwork/DataSource/DBALDataSource.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 30 | $this->connectionName = $this->connection->getDatabase(); 31 | 32 | $configuration = $this->connection->getConfiguration(); 33 | $currentLogger = $configuration->getSQLLogger(); 34 | 35 | if ($currentLogger === null) { 36 | $configuration->setSQLLogger($this); 37 | } else { 38 | $loggerChain = new LoggerChain; 39 | $loggerChain->addLogger($currentLogger); 40 | $loggerChain->addLogger($this); 41 | 42 | $configuration->setSQLLogger($loggerChain); 43 | } 44 | } 45 | 46 | // Adds executed database queries to the request 47 | public function resolve(Request $request) 48 | { 49 | $request->databaseQueries = array_merge($request->databaseQueries, $this->queries); 50 | 51 | return $request; 52 | } 53 | 54 | // Reset the data source to an empty state, clearing any collected data 55 | public function reset() 56 | { 57 | $this->queries = []; 58 | $this->query = null; 59 | } 60 | 61 | // DBAL SQLLogger event 62 | public function startQuery($sql, array $params = null, array $types = null) 63 | { 64 | $this->query = [ 65 | 'query' => $sql, 66 | 'params' => $params, 67 | 'types' => $types, 68 | 'time' => microtime(true) 69 | ]; 70 | } 71 | 72 | // DBAL SQLLogger event 73 | public function stopQuery() 74 | { 75 | $this->registerQuery($this->query); 76 | $this->query = null; 77 | } 78 | 79 | // Collect an executed database query 80 | protected function registerQuery($query) 81 | { 82 | $query = [ 83 | 'query' => $this->createRunnableQuery($query['query'], $query['params'], $query['types']), 84 | 'bindings' => $query['params'], 85 | 'duration' => (microtime(true) - $query['time']) * 1000, 86 | 'connection' => $this->connectionName, 87 | 'time' => $query['time'] 88 | ]; 89 | 90 | if ($this->passesFilters([ $query ])) { 91 | $this->queries[] = $query; 92 | } 93 | } 94 | 95 | // Takes a query, an array of params and types as arguments, returns runnable query with upper-cased keywords 96 | protected function createRunnableQuery($query, $params, $types) 97 | { 98 | // add params to query 99 | $query = $this->replaceParams($this->connection->getDatabasePlatform(), $query, $params, $types); 100 | 101 | // highlight keywords 102 | $keywords = [ 103 | 'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null', 104 | 'having', 'group by', 'order by', 'asc', 'desc' 105 | ]; 106 | $regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i'; 107 | 108 | return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query); 109 | } 110 | 111 | /** 112 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::format(). 113 | * 114 | * @param AbstractPlatform $platform 115 | * @param string $sql 116 | * @param array|null $params 117 | * @param array|null $types 118 | * 119 | * 120 | * @return string 121 | */ 122 | public function replaceParams($platform, $sql, array $params = null, array $types = null) 123 | { 124 | if (is_array($params)) { 125 | foreach ($params as $key => $param) { 126 | $type = isset($types[$key]) ? $types[$key] : null; // Originally used null coalescing 127 | $param = $this->convertParam($platform, $param, $type); 128 | $sql = preg_replace('/\?/', "$param", $sql, 1); 129 | } 130 | } 131 | return $sql; 132 | } 133 | 134 | /** 135 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::convertParam(). 136 | * 137 | * @param mixed $param 138 | * 139 | * @throws \Exception 140 | * @return string 141 | */ 142 | protected function convertParam($platform, $param, $type = null) 143 | { 144 | if (is_object($param)) { 145 | if (!method_exists($param, '__toString')) { 146 | if ($param instanceof \DateTimeInterface) { 147 | $param = $param->format('Y-m-d H:i:s'); 148 | } elseif (Type::hasType($type)) { 149 | $type = Type::getType($type); 150 | $param = $type->convertToDatabaseValue($param, $platform); 151 | } else { 152 | throw new \Exception('Given query param is an instance of ' . get_class($param) . ' and could not be converted to a string'); 153 | } 154 | } 155 | } elseif (is_array($param)) { 156 | if ($this->isNestedArray($param)) { 157 | $param = json_encode($param, JSON_UNESCAPED_UNICODE); 158 | } else { 159 | $param = implode( 160 | ', ', 161 | array_map( 162 | function ($part) { 163 | return '"' . (string) $part . '"'; 164 | }, 165 | $param 166 | ) 167 | ); 168 | return '(' . $param . ')'; 169 | } 170 | } else { 171 | $param = htmlspecialchars((string) $param); // Originally used the e() Laravel helper 172 | } 173 | return '"' . (string) $param . '"'; 174 | } 175 | 176 | /** 177 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::isNestedArray(). 178 | * 179 | * @param array $array 180 | * @return bool 181 | */ 182 | private function isNestedArray(array $array) 183 | { 184 | foreach ($array as $key => $value) { 185 | if (is_array($value)) { 186 | return true; 187 | } 188 | } 189 | return false; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Clockwork/Storage/SqlSearch.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 26 | 27 | list($this->conditions, $this->bindings) = $this->resolveConditions(); 28 | 29 | $this->buildQuery(); 30 | } 31 | 32 | // Creates a new instance from a base Search class instance 33 | public static function fromBase(Search $search = null, PDO $pdo = null) 34 | { 35 | return new static((array) $search, $pdo); 36 | } 37 | 38 | // Add an additional where condition, takes the SQL condition and array of bindings 39 | public function addCondition($condition, $bindings = []) 40 | { 41 | $this->conditions = array_merge([ $condition ], $this->conditions); 42 | $this->bindings = array_merge($bindings, $this->bindings); 43 | $this->buildQuery(); 44 | 45 | return $this; 46 | } 47 | 48 | // Resolve SQL conditions and bindings based on the search parameters 49 | protected function resolveConditions() 50 | { 51 | if ($this->isEmpty()) return [ [], [] ]; 52 | 53 | $conditions = array_filter([ 54 | $this->resolveStringCondition([ 'type' ], $this->type), 55 | $this->resolveStringCondition([ 'uri', 'commandName', 'jobName', 'testName' ], array_merge($this->uri, $this->name)), 56 | $this->resolveStringCondition([ 'controller' ], $this->controller), 57 | $this->resolveExactCondition('method', $this->method), 58 | $this->resolveNumberCondition([ 'responseStatus', 'commandExitCode', 'jobStatus', 'testStatus' ], $this->status), 59 | $this->resolveNumberCondition([ 'responseDuration' ], $this->time), 60 | $this->resolveDateCondition([ 'time' ], $this->received) 61 | ]); 62 | 63 | $sql = array_map(function ($condition) { return $condition[0]; }, $conditions); 64 | $bindings = array_reduce($conditions, function ($bindings, $condition) { 65 | return array_merge($bindings, $condition[1]); 66 | }, []); 67 | 68 | return [ $sql, $bindings ]; 69 | } 70 | 71 | // Resolve a date type condition and bindings 72 | protected function resolveDateCondition($fields, $inputs) 73 | { 74 | if (! count($inputs)) return null; 75 | 76 | $bindings = []; 77 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) { 78 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) { 79 | if (preg_match('/^<(.+)$/', $input, $match)) { 80 | $bindings["{$field}{$index}"] = $match[1]; 81 | return $this->quote($field) . " < :{$field}{$index}"; 82 | } elseif (preg_match('/^>(.+)$/', $input, $match)) { 83 | $bindings["{$field}{$index}"] = $match[1]; 84 | return $this->quote($field). " > :{$field}{$index}"; 85 | } 86 | }, $inputs, array_keys($inputs))); 87 | }, $fields)); 88 | 89 | return [ "({$conditions})", $bindings ]; 90 | } 91 | 92 | // Resolve an exact type condition and bindings 93 | protected function resolveExactCondition($field, $inputs) 94 | { 95 | if (! count($inputs)) return null; 96 | 97 | $bindings = []; 98 | $values = implode(', ', array_map(function ($input, $index) use ($field, &$bindings) { 99 | $bindings["{$field}{$index}"] = $input; 100 | return ":{$field}{$index}"; 101 | }, $inputs, array_keys($inputs))); 102 | 103 | return [ $this->quote($field) . " IN ({$values})", $bindings ]; 104 | } 105 | 106 | // Resolve a number type condition and bindings 107 | protected function resolveNumberCondition($fields, $inputs) 108 | { 109 | if (! count($inputs)) return null; 110 | 111 | $bindings = []; 112 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) { 113 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) { 114 | if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) { 115 | $bindings["{$field}{$index}"] = $match[1]; 116 | return $this->quote($field) . " < :{$field}{$index}"; 117 | } elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) { 118 | $bindings["{$field}{$index}"] = $match[1]; 119 | return $this->quote($field) . " > :{$field}{$index}"; 120 | } elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) { 121 | $bindings["{$field}{$index}from"] = $match[1]; 122 | $bindings["{$field}{$index}to"] = $match[2]; 123 | $quotedField = $this->quote($field); 124 | return "({$quotedField} > :{$field}{$index}from AND {$quotedField} < :{$field}{$index}to)"; 125 | } else { 126 | $bindings["{$field}{$index}"] = $input; 127 | return $this->quote($field) . " = :{$field}{$index}"; 128 | } 129 | }, $inputs, array_keys($inputs))); 130 | }, $fields)); 131 | 132 | return [ "({$conditions})", $bindings ]; 133 | } 134 | 135 | // Resolve a string type condition and bindings 136 | protected function resolveStringCondition($fields, $inputs) 137 | { 138 | if (! count($inputs)) return null; 139 | 140 | $bindings = []; 141 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) { 142 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) { 143 | $bindings["{$field}{$index}"] = $input; 144 | return $this->quote($field) . " LIKE :{$field}{$index}"; 145 | }, $inputs, array_keys($inputs))); 146 | }, $fields)); 147 | 148 | return [ "({$conditions})", $bindings ]; 149 | } 150 | 151 | // Build the where part of the SQL query 152 | protected function buildQuery() 153 | { 154 | $this->query = count($this->conditions) ? 'WHERE ' . implode(' AND ', $this->conditions) : ''; 155 | } 156 | 157 | // Quotes SQL identifier name properly for the current database 158 | protected function quote($identifier) 159 | { 160 | return $this->pdo && $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\""; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Clockwork/DataSource/GuzzleDataSource.php: -------------------------------------------------------------------------------- 1 | collectContent = $collectContent; 30 | $this->collectRawContent = $collectRawContent; 31 | } 32 | 33 | // Returns a new Guzzle instance, pre-configured with Clockwork support 34 | public function instance(array $config = []) 35 | { 36 | return new Client($this->configure($config)); 37 | } 38 | 39 | // Updates Guzzle configuration array with Clockwork support 40 | public function configure(array $config = []) 41 | { 42 | $handler = isset($config['handler']) ? $config['handler'] : HandlerStack::create(); 43 | 44 | $handler->push($this); 45 | 46 | $config['handler'] = $handler; 47 | 48 | return $config; 49 | } 50 | 51 | // Add sent notifications to the request 52 | public function resolve(Request $request) 53 | { 54 | $request->httpRequests = array_merge($request->httpRequests, $this->requests); 55 | 56 | return $request; 57 | } 58 | 59 | // Reset the data source to an empty state, clearing any collected data 60 | public function reset() 61 | { 62 | $this->requests = []; 63 | $this->executingRequests = []; 64 | } 65 | 66 | // Guzzle middleware implemenation, that does the requests logging itself 67 | public function __invoke(callable $handler): callable 68 | { 69 | return function(RequestInterface $request, array $options) use ($handler): PromiseInterface { 70 | $time = microtime(true); 71 | $stats = null; 72 | 73 | $originalOnStats = isset($options['on_stats']) ? $options['on_stats'] : null; 74 | $options['on_stats'] = function (TransferStats $transferStats) use (&$stats, $originalOnStats) { 75 | $stats = $transferStats->getHandlerStats(); 76 | if ($originalOnStats) $originalOnStats($transferStats); 77 | }; 78 | 79 | return $handler($request, $options) 80 | ->then(function(ResponseInterface $response) use ($request, $time, $stats) { 81 | $this->collectRequest($request, $response, $time, $stats); 82 | 83 | return $response; 84 | }, function(GuzzleException $exception) use ($request, $time, $stats) { 85 | $response = $exception instanceof RequestException ? $exception->getResponse() : null; 86 | $this->collectRequest($request, $response, $time, $stats); 87 | 88 | throw $exception; 89 | }); 90 | }; 91 | } 92 | 93 | // Collect a request-response pair 94 | protected function collectRequest($request, $response = null, $startTime = null, $stats = null) 95 | { 96 | $trace = StackTrace::get(); 97 | 98 | $request = (object) [ 99 | 'request' => (object) [ 100 | 'method' => $request->getMethod(), 101 | 'url' => $this->removeAuthFromUrl((string) $request->getUri()), 102 | 'headers' => $request->getHeaders(), 103 | 'content' => $this->collectContent ? $this->resolveRequestContent($request) : null, 104 | 'body' => $this->collectRawContent ? (string) $request->getBody() : null 105 | ], 106 | 'response' => (object) [ 107 | 'status' => (int) $response->getStatusCode(), 108 | 'headers' => $response->getHeaders(), 109 | 'content' => $this->collectContent ? json_decode((string) $response->getBody(), true) : null, 110 | 'body' => $this->collectRawContent ? (string) $response->getBody() : null 111 | ], 112 | 'stats' => $stats ? (object) [ 113 | 'timing' => isset($stats['total_time_us']) ? (object) [ 114 | 'lookup' => $stats['namelookup_time_us'] / 1000, 115 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000, 116 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000, 117 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000 118 | ] : null, 119 | 'size' => (object) [ 120 | 'upload' => isset($stats['size_upload']) ? $stats['size_upload'] : null, 121 | 'download' => isset($stats['size_download']) ? $stats['size_download'] : null 122 | ], 123 | 'speed' => (object) [ 124 | 'upload' => isset($stats['speed_upload']) ? $stats['speed_upload'] : null, 125 | 'download' => isset($stats['speed_download']) ? $stats['speed_download'] : null 126 | ], 127 | 'hosts' => (object) [ 128 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null, 129 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null 130 | ], 131 | 'version' => isset($stats['http_version']) ? $stats['http_version'] : null 132 | ] : null, 133 | 'error' => null, 134 | 'time' => $startTime, 135 | 'duration' => (microtime(true) - $startTime) * 1000, 136 | 'trace' => (new Serializer)->trace($trace) 137 | ]; 138 | 139 | if ($this->passesFilters([ $request ])) { 140 | $this->requests[] = $request; 141 | } 142 | } 143 | 144 | // Resolve request content, with support for form data and json requests 145 | protected function resolveRequestContent($request) 146 | { 147 | $body = (string) $request->getBody(); 148 | $headers = $request->getHeaders(); 149 | 150 | if (isset($headers['Content-Type']) && $headers['Content-Type'][0] == 'application/x-www-form-urlencoded') { 151 | parse_str($body, $parameters); 152 | return $parameters; 153 | } elseif (isset($headers['Content-Type']) && strpos($headers['Content-Type'][0], 'json') !== false) { 154 | return json_decode($body, true); 155 | } 156 | 157 | return []; 158 | } 159 | 160 | // Removes username and password from the URL 161 | protected function removeAuthFromUrl($url) 162 | { 163 | return preg_replace('#^(.+?://)(.+?@)(.*)$#', '$1$3', $url); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LumenDataSource.php: -------------------------------------------------------------------------------- 1 | app = $app; 33 | 34 | $this->collectLog = $collectLog; 35 | $this->collectRoutes = $collectRoutes; 36 | 37 | $this->log = new Log; 38 | } 39 | 40 | // Adds request, response information, middleware, routes, session data, user and log entries to the request 41 | public function resolve(Request $request) 42 | { 43 | $request->method = $this->getRequestMethod(); 44 | $request->uri = $this->getRequestUri(); 45 | $request->controller = $this->getController(); 46 | $request->headers = $this->getRequestHeaders(); 47 | $request->responseStatus = $this->getResponseStatus(); 48 | $request->routes = $this->getRoutes(); 49 | $request->sessionData = $this->getSessionData(); 50 | 51 | $this->resolveAuthenticatedUser($request); 52 | 53 | $request->log()->merge($this->log); 54 | 55 | return $request; 56 | } 57 | 58 | // Reset the data source to an empty state, clearing any collected data 59 | public function reset() 60 | { 61 | $this->log = new Log; 62 | } 63 | 64 | // Set Lumen response instance for the current request 65 | public function setResponse(Response $response) 66 | { 67 | $this->response = $response; 68 | return $this; 69 | } 70 | 71 | // Listen for the log events 72 | public function listenToEvents() 73 | { 74 | if (! $this->collectLog) return; 75 | 76 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) { 77 | // Lumen 5.4 78 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) { 79 | $this->log->log($event->level, $event->message, $event->context); 80 | }); 81 | } else { 82 | // Lumen 5.0 to 5.3 83 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) { 84 | $this->log->log($level, $message, $context); 85 | }); 86 | } 87 | } 88 | 89 | // Get a textual representation of current route's controller 90 | protected function getController() 91 | { 92 | $routes = method_exists($this->app, 'getRoutes') ? $this->app->getRoutes() : []; 93 | 94 | $method = $this->getRequestMethod(); 95 | $pathInfo = $this->getPathInfo(); 96 | 97 | if (isset($routes[$method.$pathInfo]['action']['uses'])) { 98 | $controller = $routes[$method.$pathInfo]['action']['uses']; 99 | } elseif (isset($routes[$method.$pathInfo]['action'][0])) { 100 | $controller = $routes[$method.$pathInfo]['action'][0]; 101 | } else { 102 | $controller = null; 103 | } 104 | 105 | if ($controller instanceof \Closure) { 106 | $controller = 'anonymous function'; 107 | } elseif (is_object($controller)) { 108 | $controller = 'instance of ' . get_class($controller); 109 | } elseif (! is_string($controller)) { 110 | $controller = null; 111 | } 112 | 113 | return $controller; 114 | } 115 | 116 | // Get the request headers 117 | protected function getRequestHeaders() 118 | { 119 | return $this->app['request']->headers->all(); 120 | } 121 | 122 | // Get the request method 123 | protected function getRequestMethod() 124 | { 125 | if ($this->app->bound('request')) { 126 | return $this->app['request']->getMethod(); 127 | } elseif (isset($_POST['_method'])) { 128 | return strtoupper($_POST['_method']); 129 | } else { 130 | return $_SERVER['REQUEST_METHOD']; 131 | } 132 | } 133 | 134 | // Get the request URI 135 | protected function getRequestUri() 136 | { 137 | return $this->app['request']->getRequestUri(); 138 | } 139 | 140 | // Get the response status code 141 | protected function getResponseStatus() 142 | { 143 | return $this->response ? $this->response->getStatusCode() : null; 144 | } 145 | 146 | // Get an array of application routes 147 | protected function getRoutes() 148 | { 149 | if (! $this->collectRoutes) return []; 150 | 151 | if (isset($this->app->router)) { 152 | $routes = array_values($this->app->router->getRoutes()); 153 | } elseif (method_exists($this->app, 'getRoutes')) { 154 | $routes = array_values($this->app->getRoutes()); 155 | } else { 156 | $routes = []; 157 | } 158 | 159 | return array_map(function ($route) { 160 | return [ 161 | 'method' => $route['method'], 162 | 'uri' => $route['uri'], 163 | 'name' => isset($route['action']['as']) ? $route['action']['as'] : null, 164 | 'action' => isset($route['action']['uses']) && is_string($route['action']['uses']) ? $route['action']['uses'] : 'anonymous function', 165 | 'middleware' => isset($route['action']['middleware']) ? $route['action']['middleware'] : null, 166 | ]; 167 | }, $routes); 168 | } 169 | 170 | // Get the session data (normalized with passwords removed) 171 | protected function getSessionData() 172 | { 173 | if (! isset($this->app['session'])) return []; 174 | 175 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all())); 176 | } 177 | 178 | // Add authenticated user data to the request 179 | protected function resolveAuthenticatedUser(Request $request) 180 | { 181 | if (! isset($this->app['auth'])) return; 182 | if (! ($user = $this->app['auth']->user())) return; 183 | if (! isset($user->email) || ! isset($user->id)) return; 184 | 185 | $request->setAuthenticatedUser($user->email, $user->id, [ 186 | 'email' => $user->email, 187 | 'name' => isset($user->name) ? $user->name : null 188 | ]); 189 | } 190 | 191 | // Get the request path info 192 | protected function getPathInfo() 193 | { 194 | if ($this->app->bound('request')) { 195 | return $this->app['request']->getPathInfo(); 196 | } else { 197 | $query = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; 198 | return '/' . trim(str_replace("?{$query}", '', $_SERVER['REQUEST_URI']), '/'); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | > Clockwork is a development tool for PHP available right in your browser. Clockwork gives you an insight into your application runtime - including request data, performance metrics, log entries, database queries, cache queries, redis commands, dispatched events, queued jobs, rendered views and more - for HTTP requests, commands, queue jobs and tests. 7 | 8 | > *This repository contains the server-side component of Clockwork.* 9 | 10 | > Check out on the [Clockwork website](https://underground.works/clockwork) for details. 11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |

38 | 39 | ### Installation 40 | 41 | Install the Clockwork library via [Composer](https://getcomposer.org/). 42 | 43 | ``` 44 | composer require itsgoingd/clockwork 45 | ``` 46 | 47 | Congratulations, you are done! To enable more features like commands or queue jobs profiling, publish the configuration file via the `vendor:publish` Artisan command. 48 | 49 | **Note:** If you are using the Laravel route cache, you will need to refresh it using the route:cache Artisan command. 50 | 51 | Read [full installation instructions](https://underground.works/clockwork/#docs-installation) on the Clockwork website. 52 | 53 | ### Features 54 | 55 | #### Collecting data 56 | 57 | The Clockwork server-side component collects and stores data about your application. 58 | 59 | Clockwork is only active when your app is in debug mode by default. You can choose to explicitly enable or disable Clockwork, or even set Clockwork to always collect data without exposing them for further analysis. 60 | 61 | We collect a whole bunch of useful data by default, but you can enable more features or disable features you don't need in the config file. 62 | 63 | Some features might allow for advanced options, eg. for database queries you can set a slow query threshold or enable detecting of duplicate (N+1) queries. Check out the config file to see all what Clockwork can do. 64 | 65 | There are several options that allow you to choose for which requests Clockwork is active. 66 | 67 | On-demand mode will collect data only when Clockwork app is open. You can even specify a secret to be set in the app settings to collect request. Errors only will record only requests ending with 4xx and 5xx responses. Slow only will collect only requests with responses above the set slow threshold. You can also filter the collected and recorded requests by a custom closure. CORS pre-flight requests will not be collected by default. 68 | 69 | New in Clockwork 4.1, artisan commands, queue jobs and tests can now also be collected, you need to enable this in the config file. 70 | 71 | Clockwork also collects stack traces for data like log messages or database queries. Last 10 frames of the trace are collected by default. You can change the frames limit or disable this feature in the configuration file. 72 | 73 | #### Viewing data 74 | 75 | ##### Web interface 76 | 77 | Visit `/clockwork` route to view and interact with the collected data. 78 | 79 | The app will show all executed requests, which is useful when the request is not made by browser, but for example a mobile application you are developing an API for. 80 | 81 | ##### Browser extension 82 | 83 | A browser dev tools extension is also available for Chrome and Firefox: 84 | 85 | - [Chrome Web Store](https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp) 86 | - [Firefox Addons](https://addons.mozilla.org/en-US/firefox/addon/clockwork-dev-tools/) 87 | 88 | ##### Toolbar 89 | 90 | Clockwork now gives you an option to show basic request information in the form of a toolbar in your app. 91 | 92 | The toolbar is fully rendered client-side and requires installing a tiny javascript library. 93 | 94 | [Learn more](https://underground.works/clockwork/#docs-viewing-data) on the Clockwork website. 95 | 96 | #### Logging 97 | 98 | You can log any variable via the clock() helper, from a simple string to an array or object, even multiple values: 99 | 100 | ```php 101 | clock(User::first(), auth()->user(), $username) 102 | ``` 103 | 104 | The `clock()` helper function returns it's first argument, so you can easily add inline debugging statements to your code: 105 | 106 | ```php 107 | User::create(clock($request->all())) 108 | ``` 109 | 110 | If you want to specify a log level, you can use the long-form call: 111 | 112 | ```php 113 | clock()->info("User {$username} logged in!") 114 | ``` 115 | 116 | #### Timeline 117 | 118 | Timeline gives you a visual representation of your application runtime. 119 | 120 | To add an event to the timeline - start it with a description, execute the tracked code and finish the event. A fluent api is available to further configure the event. 121 | 122 | ```php 123 | // using timeline api with begin/end and fluent configuration 124 | clock()->event('Importing tweets')->color('purple')->begin(); 125 | ... 126 | clock()->event('Importing tweets')->end(); 127 | ``` 128 | 129 | Alternatively you can execute the tracked code block as a closure. You can also choose to use an array based configuration instead of the fluent api. 130 | 131 | ```php 132 | // using timeline api with run and array-based configuration 133 | clock()->event('Updating cache', [ 'color' => 'green' ])->run(function () { 134 | ... 135 | }); 136 | ``` 137 | 138 | Read more about available features on the [Clockwork website](https://underground.works/clockwork). 139 | 140 |

141 | 142 | 143 | 144 |

145 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelDataSource.php: -------------------------------------------------------------------------------- 1 | app = $app; 36 | 37 | $this->collectLog = $collectLog; 38 | $this->collectRoutes = $collectRoutes; 39 | $this->routesOnlyNamespaces = $routesOnlyNamespaces; 40 | 41 | $this->log = new Log; 42 | } 43 | 44 | // Adds request, response information, middleware, routes, session data, user and log entries to the request 45 | public function resolve(Request $request) 46 | { 47 | $request->method = $this->getRequestMethod(); 48 | $request->url = $this->getRequestUrl(); 49 | $request->uri = $this->getRequestUri(); 50 | $request->controller = $this->getController(); 51 | $request->headers = $this->getRequestHeaders(); 52 | $request->responseStatus = $this->getResponseStatus(); 53 | $request->middleware = $this->getMiddleware(); 54 | $request->routes = $this->getRoutes(); 55 | $request->sessionData = $this->getSessionData(); 56 | 57 | $this->resolveAuthenticatedUser($request); 58 | 59 | $request->log()->merge($this->log); 60 | 61 | return $request; 62 | } 63 | 64 | // Reset the data source to an empty state, clearing any collected data 65 | public function reset() 66 | { 67 | $this->log = new Log; 68 | } 69 | 70 | // Set Laravel application instance for the current request 71 | public function setApplication(Application $app) 72 | { 73 | $this->app = $app; 74 | return $this; 75 | } 76 | 77 | // Set Laravel response instance for the current request 78 | public function setResponse(Response $response) 79 | { 80 | $this->response = $response; 81 | return $this; 82 | } 83 | 84 | // Listen for the log events 85 | public function listenToEvents() 86 | { 87 | if (! $this->collectLog) return; 88 | 89 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) { 90 | // Laravel 5.4 91 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) { 92 | $this->log->log($event->level, $event->message, $event->context); 93 | }); 94 | } else { 95 | // Laravel 5.0 to 5.3 96 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) { 97 | $this->log->log($level, $message, $context); 98 | }); 99 | } 100 | } 101 | 102 | // Get a textual representation of the current route's controller 103 | protected function getController() 104 | { 105 | $router = $this->app['router']; 106 | 107 | $route = $router->current(); 108 | $controller = $route ? $route->getActionName() : null; 109 | 110 | if ($controller instanceof \Closure) { 111 | $controller = 'anonymous function'; 112 | } elseif (is_object($controller)) { 113 | $controller = 'instance of ' . get_class($controller); 114 | } elseif (is_array($controller) && count($controller) == 2) { 115 | if (is_object($controller[0])) { 116 | $controller = get_class($controller[0]) . '->' . $controller[1]; 117 | } else { 118 | $controller = $controller[0] . '::' . $controller[1]; 119 | } 120 | } elseif (! is_string($controller)) { 121 | $controller = null; 122 | } 123 | 124 | return $controller; 125 | } 126 | 127 | // Get the request headers 128 | protected function getRequestHeaders() 129 | { 130 | return $this->app['request']->headers->all(); 131 | } 132 | 133 | // Get the request method 134 | protected function getRequestMethod() 135 | { 136 | return $this->app['request']->getMethod(); 137 | } 138 | 139 | // Get the request URL 140 | protected function getRequestUrl() 141 | { 142 | return $this->app['request']->fullUrl(); 143 | } 144 | 145 | // Get the request URI 146 | protected function getRequestUri() 147 | { 148 | return $this->app['request']->getRequestUri(); 149 | } 150 | 151 | // Get the response status code 152 | protected function getResponseStatus() 153 | { 154 | return $this->response ? $this->response->getStatusCode() : null; 155 | } 156 | 157 | // Get an array of middleware for the matched route 158 | protected function getMiddleware() 159 | { 160 | $route = $this->app['router']->current(); 161 | 162 | if (! $route) return; 163 | 164 | return method_exists($route, 'gatherMiddleware') ? $route->gatherMiddleware() : $route->middleware(); 165 | } 166 | 167 | // Get an array of application routes 168 | protected function getRoutes() 169 | { 170 | if (! $this->collectRoutes) return []; 171 | 172 | return array_values(array_filter(array_map(function ($route) { 173 | $action = $route->getActionName() ?: 'anonymous function'; 174 | $namespace = strpos($action, '\\') !== false ? explode('\\', $action)[0] : null; 175 | 176 | if (count($this->routesOnlyNamespaces) && ! in_array($namespace, $this->routesOnlyNamespaces)) return; 177 | 178 | return [ 179 | 'method' => implode(', ', $route->methods()), 180 | 'uri' => $route->uri(), 181 | 'name' => $route->getName(), 182 | 'action' => $action, 183 | 'middleware' => $route->middleware(), 184 | 'before' => method_exists($route, 'beforeFilters') ? implode(', ', array_keys($route->beforeFilters())) : '', 185 | 'after' => method_exists($route, 'afterFilters') ? implode(', ', array_keys($route->afterFilters())) : '' 186 | ]; 187 | }, $this->app['router']->getRoutes()->getRoutes()))); 188 | } 189 | 190 | // Get the session data (normalized with removed passwords) 191 | protected function getSessionData() 192 | { 193 | if (! isset($this->app['session'])) return []; 194 | 195 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all())); 196 | } 197 | 198 | // Add authenticated user data to the request 199 | protected function resolveAuthenticatedUser(Request $request) 200 | { 201 | if (! isset($this->app['auth'])) return; 202 | if (! ($user = $this->app['auth']->user())) return; 203 | 204 | if ($user instanceof \Illuminate\Database\Eloquent\Model) { 205 | // retrieve attributes in this awkward way to make sure we don't trigger exceptions with Eloquent strict mode on 206 | $keyName = method_exists($user, 'getAuthIdentifierName') ? $user->getAuthIdentifierName() : $user->getKeyName(); 207 | $user = $user->getAttributes(); 208 | 209 | $userId = isset($user[$keyName]) ? $user[$keyName] : null; 210 | $userEmail = isset($user['email']) ? $user['email'] : $userId; 211 | $userName = isset($user['name']) ? $user['name'] : null; 212 | } else { 213 | $userId = $user->getAuthIdentifier(); 214 | $userEmail = isset($user->email) ? $user->email : $userId; 215 | $userName = isset($user->name) ? $user->name : null; 216 | } 217 | 218 | $request->setAuthenticatedUser($userEmail, $userId, [ 219 | 'email' => $userEmail, 220 | 'name' => $userName 221 | ]); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Clockwork/Clockwork.php: -------------------------------------------------------------------------------- 1 | request = new Request; 43 | $this->authenticator = new NullAuthenticator; 44 | 45 | $this->shouldCollect = new ShouldCollect; 46 | $this->shouldRecord = new ShouldRecord; 47 | } 48 | 49 | // Add a new data source 50 | public function addDataSource(DataSourceInterface $dataSource) 51 | { 52 | $this->dataSources[] = $dataSource; 53 | return $this; 54 | } 55 | 56 | // Resolve the current request, sending it through all data sources, finalizing log and timeline 57 | public function resolveRequest() 58 | { 59 | foreach ($this->dataSources as $dataSource) { 60 | $dataSource->resolve($this->request); 61 | } 62 | 63 | $this->request->log()->sort(); 64 | $this->request->timeline()->finalize($this->request->time); 65 | 66 | return $this; 67 | } 68 | 69 | // Resolve the current request as a "command" type request with command-specific data 70 | public function resolveAsCommand($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null) 71 | { 72 | $this->resolveRequest(); 73 | 74 | $this->request->type = RequestType::COMMAND; 75 | $this->request->commandName = $name; 76 | $this->request->commandArguments = $arguments; 77 | $this->request->commandArgumentsDefaults = $argumentsDefaults; 78 | $this->request->commandOptions = $options; 79 | $this->request->commandOptionsDefaults = $optionsDefaults; 80 | $this->request->commandExitCode = $exitCode; 81 | $this->request->commandOutput = $output; 82 | 83 | return $this; 84 | } 85 | 86 | // Resolve the current request as a "queue-job" type request with queue-job-specific data 87 | public function resolveAsQueueJob($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = []) 88 | { 89 | $this->resolveRequest(); 90 | 91 | $this->request->type = RequestType::QUEUE_JOB; 92 | $this->request->jobName = $name; 93 | $this->request->jobDescription = $description; 94 | $this->request->jobStatus = $status; 95 | $this->request->jobPayload = (new Serializer)->normalize($payload); 96 | $this->request->jobQueue = $queue; 97 | $this->request->jobConnection = $connection; 98 | $this->request->jobOptions = (new Serializer)->normalizeEach($options); 99 | 100 | return $this; 101 | } 102 | 103 | // Resolve the current request as a "test" type request with test-specific data, accepts test name, status, status 104 | // message in case of failure and array of ran asserts 105 | public function resolveAsTest($name, $status = 'passed', $statusMessage = null, $asserts = []) 106 | { 107 | $this->resolveRequest(); 108 | 109 | $this->request->type = RequestType::TEST; 110 | $this->request->testName = $name; 111 | $this->request->testStatus = $status; 112 | $this->request->testStatusMessage = $statusMessage; 113 | 114 | foreach ($asserts as $assert) { 115 | $this->request->addTestAssert($assert['name'], $assert['arguments'], $assert['passed'], $assert['trace']); 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | // Extends the request with an additional data form all data sources, which is not required for normal use 122 | public function extendRequest(Request $request = null) 123 | { 124 | foreach ($this->dataSources as $dataSource) { 125 | $dataSource->extend($request ?: $this->request); 126 | } 127 | 128 | return $this; 129 | } 130 | 131 | // Store the current request via configured storage implementation 132 | public function storeRequest() 133 | { 134 | return $this->storage->store($this->request); 135 | } 136 | 137 | // Reset all data sources to an empty state, clearing any collected data 138 | public function reset() 139 | { 140 | foreach ($this->dataSources as $dataSource) { 141 | $dataSource->reset(); 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | // Get or set the current request instance 148 | public function request(Request $request = null) 149 | { 150 | if (! $request) return $this->request; 151 | 152 | $this->request = $request; 153 | return $this; 154 | } 155 | 156 | // Get the log instance for the current request or log a new message 157 | public function log($level = null, $message = null, array $context = []) 158 | { 159 | if ($level) { 160 | return $this->request->log()->log($level, $message, $context); 161 | } 162 | 163 | return $this->request->log(); 164 | } 165 | 166 | // Get the timeline instance for the current request 167 | public function timeline() 168 | { 169 | return $this->request->timeline(); 170 | } 171 | 172 | // Shortcut to create a new event on the current timeline instance 173 | public function event($description, $data = []) 174 | { 175 | return $this->request->timeline()->event($description, $data); 176 | } 177 | 178 | // Configure which requests should be collected, can be called with arrey of options, a custom closure or with no 179 | // arguments for a fluent configuration api 180 | public function shouldCollect($shouldCollect = null) 181 | { 182 | if ($shouldCollect instanceof Closure) return $this->shouldCollect->callback($shouldCollect); 183 | 184 | if (is_array($shouldCollect)) return $this->shouldCollect->merge($shouldCollect); 185 | 186 | return $this->shouldCollect; 187 | } 188 | 189 | // Configure which requests should be recorded, can be called with arrey of options, a custom closure or with no 190 | // arguments for a fluent configuration api 191 | public function shouldRecord($shouldRecord = null) 192 | { 193 | if ($shouldRecord instanceof Closure) return $this->shouldRecord->callback($shouldRecord); 194 | 195 | if (is_array($shouldRecord)) return $this->shouldRecord->merge($shouldRecord); 196 | 197 | return $this->shouldRecord; 198 | } 199 | 200 | // Get or set all data sources at once 201 | public function dataSources($dataSources = null) 202 | { 203 | if (! $dataSources) return $this->dataSources; 204 | 205 | $this->dataSources = $dataSources; 206 | return $this; 207 | } 208 | 209 | // Get or set a storage implementation 210 | public function storage(StorageInterface $storage = null) 211 | { 212 | if (! $storage) return $this->storage; 213 | 214 | $this->storage = $storage; 215 | return $this; 216 | } 217 | 218 | // Get or set an authenticator implementation 219 | public function authenticator(AuthenticatorInterface $authenticator = null) 220 | { 221 | if (! $authenticator) return $this->authenticator; 222 | 223 | $this->authenticator = $authenticator; 224 | return $this; 225 | } 226 | 227 | // Forward any other method calls to the current request and log instances 228 | public function __call($method, $args) 229 | { 230 | if (in_array($method, [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ])) { 231 | return $this->request->log()->$method(...$args); 232 | } 233 | 234 | return $this->request->$method(...$args); 235 | } 236 | 237 | // DEPRECATED The following apis are deprecated and will be removed in a future version 238 | 239 | // Get all added data sources 240 | public function getDataSources() 241 | { 242 | return $this->dataSources; 243 | } 244 | 245 | // Get the current request instance 246 | public function getRequest() 247 | { 248 | return $this->request; 249 | } 250 | 251 | // Set the current request instance 252 | public function setRequest(Request $request) 253 | { 254 | $this->request = $request; 255 | return $this; 256 | } 257 | 258 | // Get a storage implementation 259 | public function getStorage() 260 | { 261 | return $this->storage; 262 | } 263 | 264 | // Set a storage implementation 265 | public function setStorage(StorageInterface $storage) 266 | { 267 | $this->storage = $storage; 268 | return $this; 269 | } 270 | 271 | // Get an authenticator implementation 272 | public function getAuthenticator() 273 | { 274 | return $this->authenticator; 275 | } 276 | 277 | // Set an authenticator implementation 278 | public function setAuthenticator(AuthenticatorInterface $authenticator) 279 | { 280 | $this->authenticator = $authenticator; 281 | return $this; 282 | } 283 | } 284 | --------------------------------------------------------------------------------