├── public
├── favicon.ico
└── mix-manifest.json
├── src
├── Contracts
│ ├── ClearableRepository.php
│ ├── TerminableRepository.php
│ ├── PrunableRepository.php
│ └── EntriesRepository.php
├── FormatModel.php
├── Http
│ ├── Middleware
│ │ └── Authorize.php
│ ├── Controllers
│ │ ├── MailHtmlController.php
│ │ ├── LogController.php
│ │ ├── MailController.php
│ │ ├── CacheController.php
│ │ ├── GatesController.php
│ │ ├── RedisController.php
│ │ ├── ViewsController.php
│ │ ├── EventsController.php
│ │ ├── ModelsController.php
│ │ ├── QueriesController.php
│ │ ├── CommandsController.php
│ │ ├── RequestsController.php
│ │ ├── HomeController.php
│ │ ├── ScheduleController.php
│ │ ├── NotificationsController.php
│ │ ├── MailEmlController.php
│ │ ├── RecordingController.php
│ │ ├── QueueController.php
│ │ ├── MonitoredTagController.php
│ │ ├── ExceptionController.php
│ │ ├── EntryController.php
│ │ └── DumpController.php
│ └── routes.php
├── Watchers
│ ├── FormatsClosure.php
│ ├── Watcher.php
│ ├── FetchesStackTrace.php
│ ├── CommandWatcher.php
│ ├── LogWatcher.php
│ ├── DumpWatcher.php
│ ├── ModelWatcher.php
│ ├── GateWatcher.php
│ ├── ScheduleWatcher.php
│ ├── RedisWatcher.php
│ ├── NotificationWatcher.php
│ ├── ExceptionWatcher.php
│ ├── MailWatcher.php
│ ├── QueryWatcher.php
│ ├── CacheWatcher.php
│ ├── EventWatcher.php
│ └── JobWatcher.php
├── EntryType.php
├── ExtractsMailableTags.php
├── Storage
│ ├── factories
│ │ └── EntryModelFactory.php
│ ├── migrations
│ │ └── 2018_08_08_100000_create_telescope_entries_table.php
│ └── EntryQueryOptions.php
├── Console
│ ├── ClearCommand.php
│ ├── PruneCommand.php
│ ├── PublishCommand.php
│ └── InstallCommand.php
├── AuthorizesRequests.php
├── ExtractProperties.php
├── RegistersWatchers.php
├── Avatar.php
├── TelescopeApplicationServiceProvider.php
├── ExceptionContext.php
├── IncomingExceptionEntry.php
├── IncomingDumpEntry.php
├── EntryUpdate.php
├── ListensForStorageOpportunities.php
├── EntryResult.php
└── TelescopeServiceProvider.php
├── resources
├── js
│ ├── components
│ │ ├── ExceptionCodePreview.vue
│ │ ├── Stacktrace.vue
│ │ └── Alert.vue
│ ├── screens
│ │ ├── redis
│ │ │ ├── preview.vue
│ │ │ └── index.vue
│ │ ├── notifications
│ │ │ ├── preview.vue
│ │ │ └── index.vue
│ │ ├── schedule
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── commands
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── models
│ │ │ ├── preview.vue
│ │ │ └── index.vue
│ │ ├── events
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── views
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── cache
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── gates
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── queries
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── logs
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── jobs
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── mail
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ ├── requests
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ │ └── exceptions
│ │ │ ├── index.vue
│ │ │ └── preview.vue
│ ├── mixins
│ │ └── entriesStyles.js
│ ├── app.js
│ ├── base.js
│ └── routes.js
└── sass
│ ├── syntaxhighlight.scss
│ ├── app.scss
│ └── app-dark.scss
├── LICENSE.md
├── webpack.mix.js
├── composer.json
├── package.json
├── stubs
└── TelescopeServiceProvider.stub
└── README.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctfang/telescope/3.x/public/favicon.ico
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/app.js": "/app.js?id=b0a54caa17bc0ac7c25c",
3 | "/app.css": "/app.css?id=11fa83493c95c672325c",
4 | "/app-dark.css": "/app-dark.css?id=9f68f3c353c3417fd043"
5 | }
6 |
--------------------------------------------------------------------------------
/src/Contracts/ClearableRepository.php:
--------------------------------------------------------------------------------
1 | getKey()));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Http/Middleware/Authorize.php:
--------------------------------------------------------------------------------
1 |
2 | export default {
3 | props: ['lines', 'highlightedLine'],
4 | }
5 |
6 |
7 |
8 |
9 | {{number}} {{line}}
12 |
13 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/src/Http/Controllers/MailHtmlController.php:
--------------------------------------------------------------------------------
1 | find($id)->content['html'];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Http/Controllers/LogController.php:
--------------------------------------------------------------------------------
1 | Telescope::$useDarkTheme ? 'app-dark.css' : 'app.css',
19 | 'telescopeScriptVariables' => Telescope::scriptVariables(),
20 | 'assetsAreCurrent' => Telescope::assetsAreCurrent(),
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ScheduleController.php:
--------------------------------------------------------------------------------
1 | getFileName(),
24 | $listener->getStartLine(),
25 | $listener->getEndLine()
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/EntryType.php:
--------------------------------------------------------------------------------
1 | options = $options;
23 | }
24 |
25 | /**
26 | * Register the watcher.
27 | *
28 | * @param \Illuminate\Contracts\Foundation\Application $app
29 | * @return void
30 | */
31 | abstract public function register($app);
32 | }
33 |
--------------------------------------------------------------------------------
/src/ExtractsMailableTags.php:
--------------------------------------------------------------------------------
1 | ExtractTags::from($mailable),
20 | '__telescope_mailable' => get_class($mailable),
21 | '__telescope_queued' => in_array(ShouldQueue::class, class_implements($mailable)),
22 | ];
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Http/Controllers/MailEmlController.php:
--------------------------------------------------------------------------------
1 | find($id)->content['raw'], 200, [
20 | 'Content-Type' => 'message/rfc822',
21 | 'Content-Disposition' => 'attachment; filename="mail-'.$id.'.eml"',
22 | ]);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Storage/factories/EntryModelFactory.php:
--------------------------------------------------------------------------------
1 | define(EntryModel::class, function (Faker\Generator $faker) {
8 | return [
9 | 'sequence' => random_int(1, 10000),
10 | 'uuid' => $faker->uuid,
11 | 'batch_id' => $faker->uuid,
12 | 'type' => $faker->randomElement([
13 | EntryType::CACHE, EntryType::COMMAND, EntryType::DUMP, EntryType::EVENT, EntryType::EXCEPTION,
14 | EntryType::JOB, EntryType::LOG, EntryType::MAIL, EntryType::MODEL, EntryType::NOTIFICATION,
15 | EntryType::QUERY, EntryType::REDIS, EntryType::REQUEST, EntryType::SCHEDULED_TASK,
16 | ]),
17 | 'content' => [$faker->word => $faker->word],
18 | ];
19 | });
20 |
--------------------------------------------------------------------------------
/src/Console/ClearCommand.php:
--------------------------------------------------------------------------------
1 | clear();
33 |
34 | $this->info('Telescope entries cleared!');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AuthorizesRequests.php:
--------------------------------------------------------------------------------
1 | environment('local');
37 | })($request);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Console/PruneCommand.php:
--------------------------------------------------------------------------------
1 | info($repository->prune(now()->subHours($this->option('hours'))).' entries pruned.');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Console/PublishCommand.php:
--------------------------------------------------------------------------------
1 | call('vendor:publish', [
31 | '--tag' => 'telescope-config',
32 | '--force' => $this->option('force'),
33 | ]);
34 |
35 | $this->call('vendor:publish', [
36 | '--tag' => 'telescope-assets',
37 | '--force' => true,
38 | ]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/resources/js/components/Stacktrace.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 | {{line.file}}:{{line.line}}
30 |
31 |
32 |
33 | Show All
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/resources/sass/syntaxhighlight.scss:
--------------------------------------------------------------------------------
1 | .vjs-tree {
2 | // Fallback for missing fonts from vue-json-pretty on Ubuntu
3 | font-family: Monaco, Menlo, Consolas, Bitstream Vera Sans Mono, monospace !important;
4 |
5 | .vjs-tree__content {
6 | border-left: 1px dotted rgba(204, 204, 204, 0.28) !important;
7 | }
8 | .vjs-tree__node {
9 | cursor: pointer;
10 | &:hover {
11 | color: #20a0ff;
12 | }
13 | }
14 | .vjs-checkbox {
15 | position: absolute;
16 | left: -30px;
17 | }
18 | .vjs-value__null {
19 | color: #a291f5 !important;
20 | }
21 | .vjs-value__number,
22 | .vjs-value__boolean {
23 | color: #a291f5 !important;
24 | }
25 | .vjs-value__string {
26 | color: #dacb4d !important;
27 | }
28 | }
29 |
30 | .hljs-keyword,
31 | .hljs-selector-tag,
32 | .hljs-addition {
33 | color: #8bd72f;
34 | }
35 |
36 | .hljs-string,
37 | .hljs-meta .hljs-meta-string,
38 | .hljs-doctag,
39 | .hljs-regexp {
40 | color: #dacb4d;
41 | }
42 |
43 | .hljs-number,
44 | .hljs-literal {
45 | color: #a291f5 !important;
46 | }
47 |
--------------------------------------------------------------------------------
/src/Http/Controllers/RecordingController.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
26 | }
27 |
28 | /**
29 | * Toggle recording.
30 | *
31 | * @return void
32 | */
33 | public function toggle()
34 | {
35 | if ($this->cache->get('telescope:pause-recording')) {
36 | $this->cache->forget('telescope:pause-recording');
37 | } else {
38 | $this->cache->put('telescope:pause-recording', true, now()->addDays(30));
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Watchers/FetchesStackTrace.php:
--------------------------------------------------------------------------------
1 | forget(0);
17 |
18 | return $trace->first(function ($frame) {
19 | if (! isset($frame['file'])) {
20 | return false;
21 | }
22 |
23 | return ! Str::contains($frame['file'],
24 | base_path('vendor'.DIRECTORY_SEPARATOR.$this->ignoredVendorPath())
25 | );
26 | });
27 | }
28 |
29 | /**
30 | * Choose the frame outside of either Telescope/Laravel or all packages.
31 | *
32 | * @return string|null
33 | */
34 | protected function ignoredVendorPath()
35 | {
36 | if (! ($this->options['ignore_packages'] ?? true)) {
37 | return 'laravel';
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Taylor Otwell
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/resources/sass/app.scss:
--------------------------------------------------------------------------------
1 | $font-family-base: Nunito, sans-serif;
2 |
3 | $font-size-base: 0.95rem;
4 | $badge-font-size: 0.95rem;
5 |
6 | $primary: #4040c8;
7 | $secondary: #dae1e7;
8 | $success: #51d88a;
9 | $info: #bcdefa;
10 | $warning: #fff382;
11 | $danger: #ef5753;
12 |
13 | $body-bg: #edf1f3;
14 |
15 | $btn-focus-width: 0;
16 |
17 | $sidebar-nav-color: #2a5164;
18 | $sidebar-icon-color: #c3cbd3;
19 |
20 | $pill-link-active: $primary;
21 |
22 | $border-color: #efefef;
23 | $table-headers-color: #f3f4f6;
24 | $table-border-color: #efefef;
25 | $table-hover-bg: #f1f7fa;
26 |
27 | $header-border-color: #d5dfe9;
28 |
29 | $card-cap-bg: #fff;
30 | $card-bg-secondary: #f1f7fa;
31 | $card-shadow-color: #cdd8df;
32 |
33 | $code-bg: #120f12;
34 |
35 | $paginator-button-color: #9ea7ac;
36 |
37 | $new-entries-bg: #fffee9;
38 |
39 | $control-action-icon-color: #ccd2df;
40 |
41 | $grid-breakpoints: (
42 | xs: 0,
43 | sm: 2px,
44 | md: 8px,
45 | lg: 9px,
46 | xl: 10px
47 | ) !default;
48 |
49 | $container-max-widths: (
50 | sm: 1137px,
51 | md: 1138px,
52 | lg: 1139px,
53 | xl: 1140px
54 | ) !default;
55 |
56 | @import 'base';
57 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix');
2 | const webpack = require('webpack');
3 |
4 | /*
5 | |--------------------------------------------------------------------------
6 | | Mix Asset Management
7 | |--------------------------------------------------------------------------
8 | |
9 | | Mix provides a clean, fluent API for defining some Webpack build steps
10 | | for your Laravel application. By default, we are compiling the Sass
11 | | file for the application as well as bundling up all the JS files.
12 | |
13 | */
14 |
15 | mix.options({
16 | terser: {
17 | terserOptions: {
18 | compress: {
19 | drop_console: true,
20 | },
21 | },
22 | },
23 | })
24 | .setPublicPath('public')
25 | .js('resources/js/app.js', 'public')
26 | .sass('resources/sass/app.scss', 'public')
27 | .sass('resources/sass/app-dark.scss', 'public')
28 | .version()
29 | .copy('public', '../telescopetest/public/vendor/telescope')
30 | .webpackConfig({
31 | resolve: {
32 | symlinks: false,
33 | alias: {
34 | '@': path.resolve(__dirname, 'resources/js/'),
35 | },
36 | },
37 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)],
38 | });
39 |
--------------------------------------------------------------------------------
/resources/js/screens/redis/preview.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | Connection
17 |
18 | {{slotProps.entry.content.connection}}
19 |
20 |
21 |
22 |
23 | Duration
24 |
25 | {{slotProps.entry.content.time}}ms
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
{{slotProps.entry.content.command}}
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/ExtractProperties.php:
--------------------------------------------------------------------------------
1 | getProperties())
21 | ->mapWithKeys(function ($property) use ($target) {
22 | $property->setAccessible(true);
23 |
24 | if (($value = $property->getValue($target)) instanceof Model) {
25 | return [$property->getName() => FormatModel::given($value)];
26 | } elseif (is_object($value)) {
27 | return [
28 | $property->getName() => [
29 | 'class' => get_class($value),
30 | 'properties' => json_decode(json_encode($value), true),
31 | ],
32 | ];
33 | } else {
34 | return [$property->getName() => json_decode(json_encode($value), true)];
35 | }
36 | })->toArray();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/RegistersWatchers.php:
--------------------------------------------------------------------------------
1 | $watcher) {
34 | if (is_string($key) && $watcher === false) {
35 | continue;
36 | }
37 |
38 | if (is_array($watcher) && ! ($watcher['enabled'] ?? true)) {
39 | continue;
40 | }
41 |
42 | $watcher = $app->make(is_string($key) ? $key : $watcher, [
43 | 'options' => is_array($watcher) ? $watcher : [],
44 | ]);
45 |
46 | static::$watchers[] = get_class($watcher);
47 |
48 | $watcher->register($app);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/resources/js/screens/notifications/preview.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | Channel
17 |
18 | {{slotProps.entry.content.channel}}
19 |
20 |
21 |
22 |
23 | Notification
24 |
25 | {{slotProps.entry.content.notification}}
26 |
27 |
28 | Queued
29 |
30 |
31 |
32 |
33 |
34 | Notifiable
35 |
36 | {{slotProps.entry.content.notifiable}}
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/Http/Controllers/QueueController.php:
--------------------------------------------------------------------------------
1 | find($id);
32 |
33 | return response()->json([
34 | 'entry' => $entry,
35 | 'batch' => isset($entry->content['updated_batch_id'])
36 | ? $storage->get(null, EntryQueryOptions::forBatchId($entry->content['updated_batch_id']))
37 | : null,
38 | ]);
39 | }
40 |
41 | /**
42 | * The watcher class for the controller.
43 | *
44 | * @return string
45 | */
46 | protected function watcher()
47 | {
48 | return JobWatcher::class;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel/telescope",
3 | "description": "An elegant debug assistant for the Laravel framework.",
4 | "keywords": ["laravel", "monitoring", "debugging"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Taylor Otwell",
9 | "email": "taylor@laravel.com"
10 | },
11 | {
12 | "name": "Mohamed Said",
13 | "email": "mohamed@laravel.com"
14 | }
15 | ],
16 | "require": {
17 | "php": "^7.2",
18 | "ext-json": "*",
19 | "laravel/framework": "^6.0|^7.0",
20 | "moontoast/math": "^1.1",
21 | "symfony/var-dumper": "^4.4|^5.0"
22 | },
23 | "require-dev": {
24 | "ext-gd": "*",
25 | "orchestra/testbench": "^4.0|^5.0"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Laravel\\Telescope\\": "src/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Laravel\\Telescope\\Tests\\": "tests/"
35 | }
36 | },
37 | "extra": {
38 | "branch-alias": {
39 | "dev-master": "3.x-dev"
40 | },
41 | "laravel": {
42 | "providers": [
43 | "Laravel\\Telescope\\TelescopeServiceProvider"
44 | ]
45 | }
46 | },
47 | "config": {
48 | "sort-packages": true
49 | },
50 | "minimum-stability": "dev",
51 | "prefer-stable": true
52 | }
53 |
--------------------------------------------------------------------------------
/src/Avatar.php:
--------------------------------------------------------------------------------
1 | authorization();
18 | }
19 |
20 | /**
21 | * Configure the Telescope authorization services.
22 | *
23 | * @return void
24 | */
25 | protected function authorization()
26 | {
27 | $this->gate();
28 |
29 | Telescope::auth(function ($request) {
30 | return app()->environment('local') ||
31 | Gate::check('viewTelescope', [$request->user()]);
32 | });
33 | }
34 |
35 | /**
36 | * Register the Telescope gate.
37 | *
38 | * This gate determines who can access Telescope in non-local environments.
39 | *
40 | * @return void
41 | */
42 | protected function gate()
43 | {
44 | Gate::define('viewTelescope', function ($user) {
45 | return in_array($user->email, [
46 | //
47 | ]);
48 | });
49 | }
50 |
51 | /**
52 | * Register any application services.
53 | *
54 | * @return void
55 | */
56 | public function register()
57 | {
58 | //
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
6 | "watch": "npm run development -- --watch",
7 | "watch-poll": "npm run watch -- --watch-poll",
8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
9 | "prod": "npm run production",
10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
11 | },
12 | "devDependencies": {
13 | "axios": "^0.18",
14 | "bootstrap": "^4.5.0",
15 | "cross-env": "^5.1",
16 | "highlight.js": "^9.12.0",
17 | "jquery": "^3.5",
18 | "laravel-mix": "^5.0.4",
19 | "lodash": "^4.17.19",
20 | "moment": "^2.10.6",
21 | "moment-timezone": "^0.5.21",
22 | "popper.js": "^1.12",
23 | "resolve-url-loader": "^2.3.1",
24 | "sass": "^1.15.2",
25 | "sass-loader": "^7.1.0",
26 | "sql-formatter": "^2.3.1",
27 | "vue": "^2.5.7",
28 | "vue-json-pretty": "^1.6.2",
29 | "vue-router": "^3.0.1",
30 | "vue-template-compiler": "^2.5.21"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/ExceptionContext.php:
--------------------------------------------------------------------------------
1 | getFile(), "eval()'d code")) {
31 | return [
32 | $exception->getLine() => "eval()'d code",
33 | ];
34 | }
35 | }
36 |
37 | /**
38 | * Get the exception code context from a file.
39 | *
40 | * @param \Throwable $exception
41 | * @return array
42 | */
43 | protected static function getFileContext(Throwable $exception)
44 | {
45 | return collect(explode("\n", file_get_contents($exception->getFile())))
46 | ->slice($exception->getLine() - 10, 20)
47 | ->mapWithKeys(function ($value, $key) {
48 | return [$key + 1 => $value];
49 | })->all();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/resources/js/screens/redis/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Command
9 | Duration
10 | Happened
11 |
12 |
13 |
14 |
15 |
16 | {{truncate(slotProps.entry.content.command, 80)}}
17 |
18 | {{slotProps.entry.content.time}}ms
19 |
20 |
21 | {{timeAgo(slotProps.entry.created_at)}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/IncomingExceptionEntry.php:
--------------------------------------------------------------------------------
1 | exception = $exception;
26 |
27 | parent::__construct($content);
28 | }
29 |
30 | /**
31 | * Determine if the incoming entry is a reportable exception.
32 | *
33 | * @return bool
34 | */
35 | public function isReportableException()
36 | {
37 | $handler = app(ExceptionHandler::class);
38 |
39 | return method_exists($handler, 'shouldReport')
40 | ? $handler->shouldReport($this->exception) : true;
41 | }
42 |
43 | /**
44 | * Determine if the incoming entry is an exception.
45 | *
46 | * @return bool
47 | */
48 | public function isException()
49 | {
50 | return true;
51 | }
52 |
53 | /**
54 | * Calculate the family look-up hash for the incoming entry.
55 | *
56 | * @return string
57 | */
58 | public function familyHash()
59 | {
60 | return md5($this->content['file'].$this->content['line']);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/resources/sass/app-dark.scss:
--------------------------------------------------------------------------------
1 | $font-family-base: Nunito, sans-serif;
2 |
3 | $font-size-base: 0.95rem;
4 | $badge-font-size: 0.95rem;
5 |
6 | $primary: #adadff;
7 | $secondary: #494444;
8 | $success: #1f9d55;
9 | $info: #1c3d5a;
10 | $warning: #684f1d;
11 | $danger: #621b18;
12 | $gray-800: $secondary;
13 |
14 | $body-bg: #1c1c1c;
15 | $body-color: #e2edf4;
16 |
17 | $sidebar-nav-color: #6e6b6b;
18 | $sidebar-icon-color: #9f9898;
19 |
20 | $pill-link-active: $primary;
21 |
22 | $border-color: #efefef;
23 | $table-border-color: #343434;
24 | $table-headers-color: #181818;
25 | $table-hover-bg: #343434;
26 |
27 | $header-border-color: $table-border-color;
28 |
29 | $input-bg: #242424;
30 | $input-color: #e2edf4;
31 | $input-border-color: $table-border-color;
32 |
33 | $card-cap-bg: #120f12;
34 | $card-bg-secondary: #262525;
35 | $card-bg: $card-cap-bg;
36 | $card-shadow-color: $body-bg;
37 |
38 | $code-bg: $card-bg-secondary;
39 |
40 | $paginator-button-color: #9ea7ac;
41 |
42 | $modal-content-bg: $table-headers-color;
43 | $modal-backdrop-bg: #7e7e7e;
44 | $modal-footer-border-color: $input-border-color;
45 | $modal-header-border-color: $input-border-color;
46 |
47 | $new-entries-bg: #505e4a;
48 |
49 | $control-action-icon-color: #ccd2df;
50 |
51 | $dropdown-bg: $table-headers-color;
52 | $dropdown-link-color: #fff;
53 |
54 | $grid-breakpoints: (
55 | xs: 0,
56 | sm: 2px,
57 | md: 8px,
58 | lg: 9px,
59 | xl: 10px
60 | ) !default;
61 |
62 | $container-max-widths: (
63 | sm: 1137px,
64 | md: 1138px,
65 | lg: 1139px,
66 | xl: 1140px
67 | ) !default;
68 |
69 | @import 'base';
70 |
--------------------------------------------------------------------------------
/resources/js/screens/schedule/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Command
9 | Expression
10 | Happened
11 |
12 |
13 |
14 |
15 | {{ truncate(slotProps.entry.content.description, 85) || truncate(slotProps.entry.content.command, 85) }}
16 |
17 | {{slotProps.entry.content.expression}}
18 |
19 |
20 | {{timeAgo(slotProps.entry.created_at)}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/resources/js/screens/commands/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Command
9 | Exit Code
10 | Happened
11 |
12 |
13 |
14 |
15 | {{truncate(slotProps.entry.content.command, 90)}}
16 |
17 | {{slotProps.entry.content.exit_code}}
18 |
19 |
20 | {{timeAgo(slotProps.entry.created_at)}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Http/Controllers/MonitoredTagController.php:
--------------------------------------------------------------------------------
1 | entries = $entries;
27 | }
28 |
29 | /**
30 | * Get all of the tags being monitored.
31 | *
32 | * @return \Illuminate\Http\JsonResponse
33 | */
34 | public function index()
35 | {
36 | return response()->json([
37 | 'tags' => $this->entries->monitoring(),
38 | ]);
39 | }
40 |
41 | /**
42 | * Begin monitoring the given tag.
43 | *
44 | * @param \Illuminate\Http\Request $request
45 | * @return void
46 | */
47 | public function store(Request $request)
48 | {
49 | $this->entries->monitor([$request->tag]);
50 | }
51 |
52 | /**
53 | * Stop monitoring the given tag.
54 | *
55 | * @param \Illuminate\Http\Request $request
56 | * @return void
57 | */
58 | public function destroy(Request $request)
59 | {
60 | $this->entries->stopMonitoring([$request->tag]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Watchers/CommandWatcher.php:
--------------------------------------------------------------------------------
1 | listen(CommandFinished::class, [$this, 'recordCommand']);
20 | }
21 |
22 | /**
23 | * Record an Artisan command was executed.
24 | *
25 | * @param \Illuminate\Console\Events\CommandFinished $event
26 | * @return void
27 | */
28 | public function recordCommand(CommandFinished $event)
29 | {
30 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
31 | return;
32 | }
33 |
34 | Telescope::recordCommand(IncomingEntry::make([
35 | 'command' => $event->command ?? $event->input->getArguments()['command'] ?? 'default',
36 | 'exit_code' => $event->exitCode,
37 | 'arguments' => $event->input->getArguments(),
38 | 'options' => $event->input->getOptions(),
39 | ]));
40 | }
41 |
42 | /**
43 | * Determine if the event should be ignored.
44 | *
45 | * @param mixed $event
46 | * @return bool
47 | */
48 | private function shouldIgnore($event)
49 | {
50 | return in_array($event->command, array_merge($this->options['ignore'] ?? [], [
51 | 'schedule:run',
52 | 'schedule:finish',
53 | 'package:discover',
54 | ]));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/resources/js/screens/models/preview.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | Model
25 |
26 | {{slotProps.entry.content.model}}
27 |
28 |
29 |
30 |
31 | Action
32 |
33 |
34 | {{slotProps.entry.content.action}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/IncomingDumpEntry.php:
--------------------------------------------------------------------------------
1 | first(function ($entry) {
26 | return in_array($entry->type, [EntryType::REQUEST, EntryType::JOB, EntryType::COMMAND]);
27 | });
28 |
29 | if (! $entryPoint) {
30 | return;
31 | }
32 |
33 | $this->content = array_merge($this->content, [
34 | 'entry_point_type' => $entryPoint->type,
35 | 'entry_point_uuid' => $entryPoint->uuid,
36 | 'entry_point_description' => $this->entryPointDescription($entryPoint),
37 | ]);
38 | }
39 |
40 | /**
41 | * Description for the entry point.
42 | *
43 | * @param \Laravel\Telescope\IncomingDumpEntry $entryPoint
44 | * @return string
45 | */
46 | private function entryPointDescription($entryPoint)
47 | {
48 | switch ($entryPoint->type) {
49 | case EntryType::REQUEST:
50 | return $entryPoint->content['method'].' '.$entryPoint->content['uri'];
51 |
52 | case EntryType::JOB:
53 | return $entryPoint->content['name'];
54 |
55 | case EntryType::COMMAND:
56 | return $entryPoint->content['command'];
57 | }
58 |
59 | return '';
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/resources/js/screens/events/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Name
9 | Listeners
10 | Happened
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{truncate(slotProps.entry.content.name, 80)}}
18 |
19 |
20 | Broadcast
21 |
22 |
23 |
24 | {{slotProps.entry.content.listeners.length}}
25 |
26 |
27 | {{timeAgo(slotProps.entry.created_at)}}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/resources/js/screens/views/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Name
15 | Composers
16 | Happened
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{slotProps.entry.content.name}}
25 | {{truncate(slotProps.entry.content.path, 100)}}
26 |
27 |
28 |
29 | {{slotProps.entry.content.composers ? slotProps.entry.content.composers.length : 0}}
30 |
31 |
32 | {{timeAgo(slotProps.entry.created_at)}}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ExceptionController.php:
--------------------------------------------------------------------------------
1 | find($id);
45 |
46 | if ($request->input('resolved_at') === 'now') {
47 | $update = new EntryUpdate($entry->id, $entry->type, [
48 | 'resolved_at' => Carbon::now()->toDateTimeString(),
49 | ]);
50 |
51 | $storage->update(collect([$update]));
52 |
53 | // Reload entry
54 | $entry = $storage->find($id);
55 | }
56 |
57 | return response()->json([
58 | 'entry' => $entry,
59 | 'batch' => $storage->get(null, EntryQueryOptions::forBatchId($entry->batchId)->limit(-1)),
60 | ]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/resources/js/screens/cache/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Key
15 | Action
16 | Happened
17 |
18 |
19 |
20 |
21 |
22 | {{truncate(slotProps.entry.content.key, 80)}}
23 |
24 |
25 |
26 | {{slotProps.entry.content.type}}
27 |
28 |
29 |
30 |
31 | {{timeAgo(slotProps.entry.created_at)}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/resources/js/screens/gates/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Ability
15 | Result
16 | Happened
17 |
18 |
19 |
20 |
21 |
22 | {{truncate(slotProps.entry.content.ability, 80)}}
23 |
24 |
25 |
26 | {{slotProps.entry.content.result}}
27 |
28 |
29 |
30 |
31 | {{timeAgo(slotProps.entry.created_at)}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/resources/js/screens/models/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Model
15 | Action
16 | Happened
17 |
18 |
19 |
20 |
21 |
22 | {{truncate(slotProps.entry.content.model, 70)}}
23 |
24 |
25 |
26 | {{slotProps.entry.content.action}}
27 |
28 |
29 |
30 |
31 | {{timeAgo(slotProps.entry.created_at)}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/resources/js/screens/queries/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Query
9 | Duration
10 | Happened
11 |
12 |
13 |
14 |
15 |
16 | {{truncate(slotProps.entry.content.sql, 90)}}
17 |
18 |
19 |
20 | {{slotProps.entry.content.time}}ms
21 |
22 |
23 |
24 | {{slotProps.entry.content.time}}ms
25 |
26 |
27 |
28 |
29 | {{timeAgo(slotProps.entry.created_at)}}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/resources/js/screens/logs/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Message
15 | Level
16 | Happened
17 |
18 |
19 |
20 |
21 | {{truncate(slotProps.entry.content.message, 50)}}
22 |
23 |
24 |
25 | {{slotProps.entry.content.level}}
26 |
27 |
28 |
29 |
30 | {{timeAgo(slotProps.entry.created_at)}}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/Watchers/LogWatcher.php:
--------------------------------------------------------------------------------
1 | listen(MessageLogged::class, [$this, 'recordLog']);
22 | }
23 |
24 | /**
25 | * Record a message was logged.
26 | *
27 | * @param \Illuminate\Log\Events\MessageLogged $event
28 | * @return void
29 | */
30 | public function recordLog(MessageLogged $event)
31 | {
32 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
33 | return;
34 | }
35 |
36 | Telescope::recordLog(
37 | IncomingEntry::make([
38 | 'level' => $event->level,
39 | 'message' => (string) $event->message,
40 | 'context' => Arr::except($event->context, ['telescope']),
41 | ])->tags($this->tags($event))
42 | );
43 | }
44 |
45 | /**
46 | * Extract tags from the given event.
47 | *
48 | * @param \Illuminate\Log\Events\MessageLogged $event
49 | * @return array
50 | */
51 | private function tags($event)
52 | {
53 | return $event->context['telescope'] ?? [];
54 | }
55 |
56 | /**
57 | * Determine if the event should be ignored.
58 | *
59 | * @param mixed $event
60 | * @return bool
61 | */
62 | private function shouldIgnore($event)
63 | {
64 | return isset($event->context['exception']) &&
65 | $event->context['exception'] instanceof Exception;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Watchers/DumpWatcher.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
33 | }
34 |
35 | /**
36 | * Register the watcher.
37 | *
38 | * @param \Illuminate\Contracts\Foundation\Application $app
39 | * @return void
40 | */
41 | public function register($app)
42 | {
43 | if (! $this->cache->get('telescope:dump-watcher')) {
44 | return;
45 | }
46 |
47 | $htmlDumper = new HtmlDumper();
48 | $htmlDumper->setDumpHeader('');
49 |
50 | VarDumper::setHandler(function ($var) use ($htmlDumper) {
51 | $this->recordDump($htmlDumper->dump(
52 | (new VarCloner)->cloneVar($var), true
53 | ));
54 | });
55 | }
56 |
57 | /**
58 | * Record a dumped variable.
59 | *
60 | * @param string $dump
61 | * @return void
62 | */
63 | public function recordDump($dump)
64 | {
65 | Telescope::recordDump(
66 | IncomingDumpEntry::make(['dump' => $dump])
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/resources/js/screens/gates/preview.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 | Ability
24 |
25 | {{slotProps.entry.content.ability}}
26 |
27 |
28 |
29 |
30 | Result
31 |
32 |
33 | {{slotProps.entry.content.result}}
34 |
35 |
36 |
37 |
38 |
39 | Location
40 |
41 | {{slotProps.entry.content.file}}:{{slotProps.entry.content.line}}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/Watchers/ModelWatcher.php:
--------------------------------------------------------------------------------
1 | listen($this->options['events'] ?? 'eloquent.*', [$this, 'recordAction']);
21 | }
22 |
23 | /**
24 | * Record an action.
25 | *
26 | * @param string $event
27 | * @param array $data
28 | * @return void
29 | */
30 | public function recordAction($event, $data)
31 | {
32 | if (! Telescope::isRecording() || ! $this->shouldRecord($event)) {
33 | return;
34 | }
35 |
36 | $model = FormatModel::given($data[0]);
37 |
38 | $changes = $data[0]->getChanges();
39 |
40 | Telescope::recordModelEvent(IncomingEntry::make(array_filter([
41 | 'action' => $this->action($event),
42 | 'model' => $model,
43 | 'changes' => empty($changes) ? null : $changes,
44 | ]))->tags([$model]));
45 | }
46 |
47 | /**
48 | * Extract the Eloquent action from the given event.
49 | *
50 | * @param string $event
51 | * @return mixed
52 | */
53 | private function action($event)
54 | {
55 | preg_match('/\.(.*):/', $event, $matches);
56 |
57 | return $matches[1];
58 | }
59 |
60 | /**
61 | * Determine if the Eloquent event should be recorded.
62 | *
63 | * @param string $eventName
64 | * @return bool
65 | */
66 | private function shouldRecord($eventName)
67 | {
68 | return Str::is([
69 | '*created*', '*updated*', '*restored*', '*deleted*',
70 | ], $eventName);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/stubs/TelescopeServiceProvider.stub:
--------------------------------------------------------------------------------
1 | hideSensitiveRequestDetails();
22 |
23 | Telescope::filter(function (IncomingEntry $entry) {
24 | if ($this->app->environment('local')) {
25 | return true;
26 | }
27 |
28 | return $entry->isReportableException() ||
29 | $entry->isFailedRequest() ||
30 | $entry->isFailedJob() ||
31 | $entry->isScheduledTask() ||
32 | $entry->hasMonitoredTag();
33 | });
34 | }
35 |
36 | /**
37 | * Prevent sensitive request details from being logged by Telescope.
38 | *
39 | * @return void
40 | */
41 | protected function hideSensitiveRequestDetails()
42 | {
43 | if ($this->app->environment('local')) {
44 | return;
45 | }
46 |
47 | Telescope::hideRequestParameters(['_token']);
48 |
49 | Telescope::hideRequestHeaders([
50 | 'cookie',
51 | 'x-csrf-token',
52 | 'x-xsrf-token',
53 | ]);
54 | }
55 |
56 | /**
57 | * Register the Telescope gate.
58 | *
59 | * This gate determines who can access Telescope in non-local environments.
60 | *
61 | * @return void
62 | */
63 | protected function gate()
64 | {
65 | Gate::define('viewTelescope', function ($user) {
66 | return in_array($user->email, [
67 | //
68 | ]);
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/resources/js/screens/schedule/preview.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | Description
17 |
18 | {{slotProps.entry.content.description || '-'}}
19 |
20 |
21 |
22 |
23 | Command
24 |
25 | {{slotProps.entry.content.command || '-'}}
26 |
27 |
28 |
29 |
30 | Expression
31 |
32 | {{slotProps.entry.content.expression}}
33 |
34 |
35 |
36 |
37 | User
38 |
39 | {{slotProps.entry.content.user || '-'}}
40 |
41 |
42 |
43 |
44 | Timezone
45 |
46 | {{slotProps.entry.content.timezone || '-'}}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
{{ slotProps.entry.content.output }}
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/resources/js/screens/cache/preview.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 | Action
30 |
31 |
32 | {{slotProps.entry.content.type}}
33 |
34 |
35 |
36 |
37 |
38 | Key
39 |
40 | {{slotProps.entry.content.key}}
41 |
42 |
43 |
44 |
45 | Expiration
46 |
47 | {{formatExpiration(slotProps.entry.content.expiration)}}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
{{slotProps.entry.content.value}}
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/resources/js/screens/notifications/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Notification
9 | Channel
10 | Happened
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{truncate(slotProps.entry.content.notification || '-', 70)}}
18 |
19 |
20 | Queued
21 |
22 |
23 |
24 |
25 |
26 | Recipient: {{truncate(slotProps.entry.content.notifiable, 90)}}
27 |
28 |
29 |
30 | {{truncate(slotProps.entry.content.channel, 20)}}
31 |
32 |
33 | {{timeAgo(slotProps.entry.created_at)}}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Introduction
11 |
12 | Laravel Telescope is an elegant debug assistant for the Laravel framework. Telescope provides insight into the requests coming into your application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps and more. Telescope makes a wonderful companion to your local Laravel development environment.
13 |
14 |
15 |
16 |
17 |
18 | ## Official Documentation
19 |
20 | Documentation for Telescope can be found on the [Laravel website](https://laravel.com/docs/telescope).
21 |
22 | ## Contributing
23 |
24 | Thank you for considering contributing to Telescope! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
25 |
26 | ## Code of Conduct
27 |
28 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
29 |
30 | ## Security Vulnerabilities
31 |
32 | Please review [our security policy](https://github.com/laravel/telescope/security/policy) on how to report security vulnerabilities.
33 |
34 | ## License
35 |
36 | Laravel Telescope is open-sourced software licensed under the [MIT license](LICENSE.md).
37 |
--------------------------------------------------------------------------------
/resources/js/screens/jobs/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Job
15 | Status
16 | Happened
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{truncate(slotProps.entry.content.name, 68)}}
24 |
25 | Connection: {{slotProps.entry.content.connection}} | Queue: {{slotProps.entry.content.queue}}
26 |
27 |
28 |
29 |
30 |
31 | {{slotProps.entry.content.status}}
32 |
33 |
34 |
35 |
36 | {{ timeAgo(slotProps.entry.created_at) }}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/Contracts/EntriesRepository.php:
--------------------------------------------------------------------------------
1 | = 500) return 'danger';
48 | },
49 |
50 | requestMethodClass(method) {
51 | if (method == 'GET') return 'secondary';
52 | if (method == 'OPTIONS') return 'secondary';
53 | if (method == 'POST') return 'info';
54 | if (method == 'PATCH') return 'info';
55 | if (method == 'PUT') return 'info';
56 | if (method == 'DELETE') return 'danger';
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/src/Watchers/GateWatcher.php:
--------------------------------------------------------------------------------
1 | shouldIgnore($ability)) {
40 | return;
41 | }
42 |
43 | $caller = $this->getCallerFromStackTrace();
44 |
45 | Telescope::recordGate(IncomingEntry::make([
46 | 'ability' => $ability,
47 | 'result' => $result ? 'allowed' : 'denied',
48 | 'arguments' => $this->formatArguments($arguments),
49 | 'file' => $caller['file'],
50 | 'line' => $caller['line'],
51 | ]));
52 |
53 | return $result;
54 | }
55 |
56 | /**
57 | * Determine if the ability should be ignored.
58 | *
59 | * @param string $ability
60 | * @return bool
61 | */
62 | private function shouldIgnore($ability)
63 | {
64 | return Str::is($this->options['ignore_abilities'] ?? [], $ability);
65 | }
66 |
67 | /**
68 | * Format the given arguments.
69 | *
70 | * @param array $arguments
71 | * @return array
72 | */
73 | private function formatArguments($arguments)
74 | {
75 | return collect($arguments)->map(function ($argument) {
76 | return $argument instanceof Model ? FormatModel::given($argument) : $argument;
77 | })->toArray();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Watchers/ScheduleWatcher.php:
--------------------------------------------------------------------------------
1 | listen(CommandStarting::class, [$this, 'recordCommand']);
23 | }
24 |
25 | /**
26 | * Record a scheduled command was executed.
27 | *
28 | * @param \Illuminate\Console\Events\CommandStarting $event
29 | * @return void
30 | */
31 | public function recordCommand(CommandStarting $event)
32 | {
33 | if (! Telescope::isRecording() ||
34 | $event->command !== 'schedule:run' &&
35 | $event->command !== 'schedule:finish') {
36 | return;
37 | }
38 |
39 | collect(app(Schedule::class)->events())->each(function ($event) {
40 | $event->then(function () use ($event) {
41 | Telescope::recordScheduledCommand(IncomingEntry::make([
42 | 'command' => $event instanceof CallbackEvent ? 'Closure' : $event->command,
43 | 'description' => $event->description,
44 | 'expression' => $event->expression,
45 | 'timezone' => $event->timezone,
46 | 'user' => $event->user,
47 | 'output' => $this->getEventOutput($event),
48 | ]));
49 | });
50 | });
51 | }
52 |
53 | /**
54 | * Get the output for the scheduled event.
55 | *
56 | * @param \Illuminate\Console\Scheduling\Event $event
57 | * @return string|null
58 | */
59 | protected function getEventOutput(Event $event)
60 | {
61 | if (! $event->output ||
62 | $event->output === $event->getDefaultOutput() ||
63 | $event->shouldAppendOutput ||
64 | ! file_exists($event->output)) {
65 | return '';
66 | }
67 |
68 | return trim(file_get_contents($event->output));
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/resources/js/screens/commands/preview.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 | Command
20 |
21 | {{slotProps.entry.content.command}}
22 |
23 |
24 |
25 |
26 | Exit Code
27 |
28 | {{slotProps.entry.content.exit_code}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/resources/js/screens/queries/preview.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 | Connection
29 |
30 | {{slotProps.entry.content.connection}}
31 |
32 |
33 |
34 |
35 | Location
36 |
37 | {{slotProps.entry.content.file}}:{{slotProps.entry.content.line}}
38 |
39 |
40 |
41 |
42 | Duration
43 |
44 |
45 | {{slotProps.entry.content.time}}ms
46 |
47 |
48 |
49 | {{slotProps.entry.content.time}}ms
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
{{sqlFormatter.format(slotProps.entry.content.sql)}}
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/resources/js/screens/mail/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | Mailable
18 | Recipients
19 | Happened
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{truncate(slotProps.entry.content.mailable || '-', 70)}}
27 |
28 |
29 | Queued
30 |
31 |
32 |
33 |
34 |
35 | Subject: {{truncate(slotProps.entry.content.subject, 90)}}
36 |
37 |
38 |
39 | {{recipientsCount(slotProps.entry)}}
40 |
41 |
42 | {{timeAgo(slotProps.entry.created_at)}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/EntryUpdate.php:
--------------------------------------------------------------------------------
1 | [], 'added' => []];
34 |
35 | /**
36 | * Create a new incoming entry instance.
37 | *
38 | * @param string $uuid
39 | * @param string $type
40 | * @param array $changes
41 | * @return void
42 | */
43 | public function __construct($uuid, $type, array $changes)
44 | {
45 | $this->uuid = $uuid;
46 | $this->type = $type;
47 | $this->changes = $changes;
48 | }
49 |
50 | /**
51 | * Create a new entry update instance.
52 | *
53 | * @param mixed ...$arguments
54 | * @return static
55 | */
56 | public static function make(...$arguments)
57 | {
58 | return new static(...$arguments);
59 | }
60 |
61 | /**
62 | * Set the properties that should be updated.
63 | *
64 | * @param array $changes
65 | * @return $this
66 | */
67 | public function change(array $changes)
68 | {
69 | $this->changes = array_merge($this->changes, $changes);
70 |
71 | return $this;
72 | }
73 |
74 | /**
75 | * Add tags to the entry.
76 | *
77 | * @param array $tags
78 | * @return $this
79 | */
80 | public function addTags(array $tags)
81 | {
82 | $this->tagsChanges['added'] = array_unique(
83 | array_merge($this->tagsChanges['added'], $tags)
84 | );
85 |
86 | return $this;
87 | }
88 |
89 | /**
90 | * Remove tags from the entry.
91 | *
92 | * @param array $tags
93 | * @return $this
94 | */
95 | public function removeTags(array $tags)
96 | {
97 | $this->tagsChanges['removed'] = array_unique(
98 | array_merge($this->tagsChanges['removed'], $tags)
99 | );
100 |
101 | return $this;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Http/Controllers/EntryController.php:
--------------------------------------------------------------------------------
1 | json([
36 | 'entries' => $storage->get(
37 | $this->entryType(),
38 | EntryQueryOptions::fromRequest($request)
39 | ),
40 | 'status' => $this->status(),
41 | ]);
42 | }
43 |
44 | /**
45 | * Get an entry with the given ID.
46 | *
47 | * @param \Laravel\Telescope\Contracts\EntriesRepository $storage
48 | * @param int $id
49 | * @return \Illuminate\Http\JsonResponse
50 | */
51 | public function show(EntriesRepository $storage, $id)
52 | {
53 | $entry = $storage->find($id)->generateAvatar();
54 |
55 | return response()->json([
56 | 'entry' => $entry,
57 | 'batch' => $storage->get(null, EntryQueryOptions::forBatchId($entry->batchId)->limit(-1)),
58 | ]);
59 | }
60 |
61 | /**
62 | * Determine the watcher recording status.
63 | *
64 | * @return string
65 | */
66 | protected function status()
67 | {
68 | if (! config('telescope.enabled', false)) {
69 | return 'disabled';
70 | }
71 |
72 | if (cache('telescope:pause-recording', false)) {
73 | return 'paused';
74 | }
75 |
76 | $watcher = config('telescope.watchers.'.$this->watcher());
77 |
78 | if (! $watcher || (isset($watcher['enabled']) && ! $watcher['enabled'])) {
79 | return 'off';
80 | }
81 |
82 | return 'enabled';
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/resources/js/screens/logs/preview.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 | Level
31 |
32 |
33 | {{slotProps.entry.content.level}}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
{{slotProps.entry.content.message}}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
66 |
--------------------------------------------------------------------------------
/resources/js/screens/requests/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Verb
15 | Path
16 | Status
17 | Duration
18 | Happened
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{slotProps.entry.content.method}}
27 |
28 |
29 |
30 | {{truncate(slotProps.entry.content.uri, 60)}}
31 |
32 |
33 |
34 | {{slotProps.entry.content.response_status}}
35 |
36 |
37 |
38 |
39 | {{slotProps.entry.content.duration}}ms
40 | -
41 |
42 |
43 |
44 | {{timeAgo(slotProps.entry.created_at)}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Base from './base';
3 | import axios from 'axios';
4 | import Routes from './routes';
5 | import VueRouter from 'vue-router';
6 | import VueJsonPretty from 'vue-json-pretty';
7 | import moment from 'moment-timezone';
8 |
9 | require('bootstrap');
10 |
11 | let token = document.head.querySelector('meta[name="csrf-token"]');
12 |
13 | if (token) {
14 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
15 | }
16 |
17 | Vue.use(VueRouter);
18 |
19 | window.Popper = require('popper.js').default;
20 |
21 | moment.tz.setDefault(Telescope.timezone);
22 |
23 | window.Telescope.basePath = '/' + window.Telescope.path;
24 |
25 | let routerBasePath = window.Telescope.basePath + '/';
26 |
27 | if (window.Telescope.path === '' || window.Telescope.path === '/') {
28 | routerBasePath = '/';
29 | window.Telescope.basePath = '';
30 | }
31 |
32 | const router = new VueRouter({
33 | routes: Routes,
34 | mode: 'history',
35 | base: routerBasePath,
36 | });
37 |
38 | Vue.component('vue-json-pretty', VueJsonPretty);
39 | Vue.component('related-entries', require('./components/RelatedEntries.vue').default);
40 | Vue.component('index-screen', require('./components/IndexScreen.vue').default);
41 | Vue.component('preview-screen', require('./components/PreviewScreen.vue').default);
42 | Vue.component('alert', require('./components/Alert.vue').default);
43 |
44 | Vue.mixin(Base);
45 |
46 | new Vue({
47 | el: '#telescope',
48 |
49 | router,
50 |
51 | data() {
52 | return {
53 | alert: {
54 | type: null,
55 | autoClose: 0,
56 | message: '',
57 | confirmationProceed: null,
58 | confirmationCancel: null,
59 | },
60 |
61 | autoLoadsNewEntries: localStorage.autoLoadsNewEntries === '1',
62 |
63 | recording: Telescope.recording,
64 | };
65 | },
66 |
67 | methods: {
68 | autoLoadNewEntries() {
69 | if (!this.autoLoadsNewEntries) {
70 | this.autoLoadsNewEntries = true;
71 | localStorage.autoLoadsNewEntries = 1;
72 | } else {
73 | this.autoLoadsNewEntries = false;
74 | localStorage.autoLoadsNewEntries = 0;
75 | }
76 | },
77 |
78 | toggleRecording() {
79 | axios.post(Telescope.basePath + '/telescope-api/toggle-recording');
80 |
81 | window.Telescope.recording = !Telescope.recording;
82 | this.recording = !this.recording;
83 | },
84 | },
85 | });
86 |
--------------------------------------------------------------------------------
/src/Http/Controllers/DumpController.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
33 | }
34 |
35 | /**
36 | * List the entries of the given type.
37 | *
38 | * @param \Illuminate\Http\Request $request
39 | * @param \Laravel\Telescope\Contracts\EntriesRepository $storage
40 | * @return \Illuminate\Http\JsonResponse
41 | */
42 | public function index(Request $request, EntriesRepository $storage)
43 | {
44 | $this->cache->put('telescope:dump-watcher', true, now()->addSeconds(4));
45 |
46 | return response()->json([
47 | 'dump' => (new HtmlDumper())->dump((new VarCloner)->cloneVar(true), true),
48 | 'entries' => $storage->get(
49 | $this->entryType(),
50 | EntryQueryOptions::fromRequest($request)
51 | ),
52 | 'status' => $this->status(),
53 | ]);
54 | }
55 |
56 | /**
57 | * Determine the watcher recording status.
58 | *
59 | * @return string
60 | */
61 | protected function status()
62 | {
63 | if ($this->cache->getStore() instanceof ArrayStore) {
64 | return 'wrong-cache';
65 | }
66 |
67 | return parent::status();
68 | }
69 |
70 | /**
71 | * The entry type for the controller.
72 | *
73 | * @return string
74 | */
75 | protected function entryType()
76 | {
77 | return EntryType::DUMP;
78 | }
79 |
80 | /**
81 | * The watcher class for the controller.
82 | *
83 | * @return string
84 | */
85 | protected function watcher()
86 | {
87 | return DumpWatcher::class;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Watchers/RedisWatcher.php:
--------------------------------------------------------------------------------
1 | listen(CommandExecuted::class, [$this, 'recordCommand']);
20 |
21 | foreach ((array) $app['redis']->connections() as $connection) {
22 | $connection->setEventDispatcher($app['events']);
23 | }
24 |
25 | $app['redis']->enableEvents();
26 | }
27 |
28 | /**
29 | * Record a Redis command was executed.
30 | *
31 | * @param \Illuminate\Redis\Events\CommandExecuted $event
32 | * @return void
33 | */
34 | public function recordCommand(CommandExecuted $event)
35 | {
36 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
37 | return;
38 | }
39 |
40 | Telescope::recordRedis(IncomingEntry::make([
41 | 'connection' => $event->connectionName,
42 | 'command' => $this->formatCommand($event->command, $event->parameters),
43 | 'time' => number_format($event->time, 2, '.', ''),
44 | ]));
45 | }
46 |
47 | /**
48 | * Format the given Redis command.
49 | *
50 | * @param string $command
51 | * @param array $parameters
52 | * @return string
53 | */
54 | private function formatCommand($command, $parameters)
55 | {
56 | $parameters = collect($parameters)->map(function ($parameter) {
57 | if (is_array($parameter)) {
58 | return collect($parameter)->map(function ($value, $key) {
59 | if (is_array($value)) {
60 | return json_encode($value);
61 | }
62 |
63 | return is_int($key) ? $value : "{$key} {$value}";
64 | })->implode(' ');
65 | }
66 |
67 | return $parameter;
68 | })->implode(' ');
69 |
70 | return "{$command} {$parameters}";
71 | }
72 |
73 | /**
74 | * Determine if the event should be ignored.
75 | *
76 | * @param mixed $event
77 | * @return bool
78 | */
79 | private function shouldIgnore($event)
80 | {
81 | return in_array($event->command, [
82 | 'pipeline', 'transaction',
83 | ]);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/resources/js/screens/events/preview.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | Event
18 |
19 | {{slotProps.entry.content.name}}
20 |
21 |
22 | Broadcast
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ listener.name }}
50 |
51 |
52 | Queued
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/Watchers/NotificationWatcher.php:
--------------------------------------------------------------------------------
1 | listen(NotificationSent::class, [$this, 'recordNotification']);
25 | }
26 |
27 | /**
28 | * Record a new notification message was sent.
29 | *
30 | * @param \Illuminate\Notifications\Events\NotificationSent $event
31 | * @return void
32 | */
33 | public function recordNotification(NotificationSent $event)
34 | {
35 | if (! Telescope::isRecording()) {
36 | return;
37 | }
38 |
39 | Telescope::recordNotification(IncomingEntry::make([
40 | 'notification' => get_class($event->notification),
41 | 'queued' => in_array(ShouldQueue::class, class_implements($event->notification)),
42 | 'notifiable' => $this->formatNotifiable($event->notifiable),
43 | 'channel' => $event->channel,
44 | 'response' => $event->response,
45 | ])->tags($this->tags($event)));
46 | }
47 |
48 | /**
49 | * Extract the tags for the given event.
50 | *
51 | * @param \Illuminate\Notifications\Events\NotificationSent $event
52 | * @return array
53 | */
54 | private function tags($event)
55 | {
56 | return array_merge([
57 | $this->formatNotifiable($event->notifiable),
58 | ], ExtractTags::from($event->notification));
59 | }
60 |
61 | /**
62 | * Format the given notifiable into a tag.
63 | *
64 | * @param mixed $notifiable
65 | * @return string
66 | */
67 | private function formatNotifiable($notifiable)
68 | {
69 | if ($notifiable instanceof Model) {
70 | return FormatModel::given($notifiable);
71 | } elseif ($notifiable instanceof AnonymousNotifiable) {
72 | $routes = array_map(function ($route) {
73 | return is_array($route) ? implode(',', $route) : $route;
74 | }, $notifiable->routes);
75 |
76 | return 'Anonymous:'.implode(',', $routes);
77 | }
78 |
79 | return get_class($notifiable);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Watchers/ExceptionWatcher.php:
--------------------------------------------------------------------------------
1 | listen(MessageLogged::class, [$this, 'recordException']);
24 | }
25 |
26 | /**
27 | * Record an exception was logged.
28 | *
29 | * @param \Illuminate\Log\Events\MessageLogged $event
30 | * @return void
31 | */
32 | public function recordException(MessageLogged $event)
33 | {
34 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
35 | return;
36 | }
37 |
38 | $exception = $event->context['exception'];
39 |
40 | $trace = collect($exception->getTrace())->map(function ($item) {
41 | return Arr::only($item, ['file', 'line']);
42 | })->toArray();
43 |
44 | Telescope::recordException(
45 | IncomingExceptionEntry::make($exception, [
46 | 'class' => get_class($exception),
47 | 'file' => $exception->getFile(),
48 | 'line' => $exception->getLine(),
49 | 'message' => $exception->getMessage(),
50 | 'context' => transform(Arr::except($event->context, ['exception', 'telescope']), function ($context) {
51 | return ! empty($context) ? $context : null;
52 | }),
53 | 'trace' => $trace,
54 | 'line_preview' => ExceptionContext::get($exception),
55 | ])->tags($this->tags($event))
56 | );
57 | }
58 |
59 | /**
60 | * Extract the tags for the given event.
61 | *
62 | * @param \Illuminate\Log\Events\MessageLogged $event
63 | * @return array
64 | */
65 | protected function tags($event)
66 | {
67 | return array_merge(ExtractTags::from($event->context['exception']),
68 | $event->context['telescope'] ?? []
69 | );
70 | }
71 |
72 | /**
73 | * Determine if the event should be ignored.
74 | *
75 | * @param mixed $event
76 | * @return bool
77 | */
78 | private function shouldIgnore($event)
79 | {
80 | return ! isset($event->context['exception']) ||
81 | ! $event->context['exception'] instanceof Exception;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Console/InstallCommand.php:
--------------------------------------------------------------------------------
1 | comment('Publishing Telescope Service Provider...');
32 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-provider']);
33 |
34 | $this->comment('Publishing Telescope Assets...');
35 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-assets']);
36 |
37 | $this->comment('Publishing Telescope Configuration...');
38 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-config']);
39 |
40 | $this->registerTelescopeServiceProvider();
41 |
42 | $this->info('Telescope scaffolding installed successfully.');
43 | }
44 |
45 | /**
46 | * Register the Telescope service provider in the application configuration file.
47 | *
48 | * @return void
49 | */
50 | protected function registerTelescopeServiceProvider()
51 | {
52 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace());
53 |
54 | $appConfig = file_get_contents(config_path('app.php'));
55 |
56 | if (Str::contains($appConfig, $namespace.'\\Providers\\TelescopeServiceProvider::class')) {
57 | return;
58 | }
59 |
60 | $lineEndingCount = [
61 | "\r\n" => substr_count($appConfig, "\r\n"),
62 | "\r" => substr_count($appConfig, "\r"),
63 | "\n" => substr_count($appConfig, "\n"),
64 | ];
65 |
66 | $eol = array_keys($lineEndingCount, max($lineEndingCount))[0];
67 |
68 | file_put_contents(config_path('app.php'), str_replace(
69 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol,
70 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol." {$namespace}\Providers\TelescopeServiceProvider::class,".$eol,
71 | $appConfig
72 | ));
73 |
74 | file_put_contents(app_path('Providers/TelescopeServiceProvider.php'), str_replace(
75 | "namespace App\Providers;",
76 | "namespace {$namespace}\Providers;",
77 | file_get_contents(app_path('Providers/TelescopeServiceProvider.php'))
78 | ));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Storage/migrations/2018_08_08_100000_create_telescope_entries_table.php:
--------------------------------------------------------------------------------
1 | schema = Schema::connection($this->getConnection());
24 | }
25 |
26 | /**
27 | * Get the migration connection name.
28 | *
29 | * @return string|null
30 | */
31 | public function getConnection()
32 | {
33 | return config('telescope.storage.database.connection');
34 | }
35 |
36 | /**
37 | * Run the migrations.
38 | *
39 | * @return void
40 | */
41 | public function up()
42 | {
43 | $this->schema->create('telescope_entries', function (Blueprint $table) {
44 | $table->bigIncrements('sequence');
45 | $table->uuid('uuid');
46 | $table->uuid('batch_id');
47 | $table->string('family_hash')->nullable();
48 | $table->boolean('should_display_on_index')->default(true);
49 | $table->string('type', 20);
50 | $table->longText('content');
51 | $table->dateTime('created_at')->nullable();
52 |
53 | $table->unique('uuid');
54 | $table->index('batch_id');
55 | $table->index('family_hash');
56 | $table->index('created_at');
57 | $table->index(['type', 'should_display_on_index']);
58 | });
59 |
60 | $this->schema->create('telescope_entries_tags', function (Blueprint $table) {
61 | $table->uuid('entry_uuid');
62 | $table->string('tag');
63 |
64 | $table->index(['entry_uuid', 'tag']);
65 | $table->index('tag');
66 |
67 | $table->foreign('entry_uuid')
68 | ->references('uuid')
69 | ->on('telescope_entries')
70 | ->onDelete('cascade');
71 | });
72 |
73 | $this->schema->create('telescope_monitoring', function (Blueprint $table) {
74 | $table->string('tag');
75 | });
76 | }
77 |
78 | /**
79 | * Reverse the migrations.
80 | *
81 | * @return void
82 | */
83 | public function down()
84 | {
85 | $this->schema->dropIfExists('telescope_entries_tags');
86 | $this->schema->dropIfExists('telescope_entries');
87 | $this->schema->dropIfExists('telescope_monitoring');
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Watchers/MailWatcher.php:
--------------------------------------------------------------------------------
1 | listen(MessageSent::class, [$this, 'recordMail']);
20 | }
21 |
22 | /**
23 | * Record a mail message was sent.
24 | *
25 | * @param \Illuminate\Mail\Events\MessageSent $event
26 | * @return void
27 | */
28 | public function recordMail(MessageSent $event)
29 | {
30 | if (! Telescope::isRecording()) {
31 | return;
32 | }
33 |
34 | Telescope::recordMail(IncomingEntry::make([
35 | 'mailable' => $this->getMailable($event),
36 | 'queued' => $this->getQueuedStatus($event),
37 | 'from' => $event->message->getFrom(),
38 | 'replyTo' => $event->message->getReplyTo(),
39 | 'to' => $event->message->getTo(),
40 | 'cc' => $event->message->getCc(),
41 | 'bcc' => $event->message->getBcc(),
42 | 'subject' => $event->message->getSubject(),
43 | 'html' => $event->message->getBody(),
44 | 'raw' => $event->message->toString(),
45 | ])->tags($this->tags($event->message, $event->data)));
46 | }
47 |
48 | /**
49 | * Get the name of the mailable.
50 | *
51 | * @param \Illuminate\Mail\Events\MessageSent $event
52 | * @return string
53 | */
54 | protected function getMailable($event)
55 | {
56 | if (isset($event->data['__laravel_notification'])) {
57 | return $event->data['__laravel_notification'];
58 | }
59 |
60 | return $event->data['__telescope_mailable'] ?? '';
61 | }
62 |
63 | /**
64 | * Determine whether the mailable was queued.
65 | *
66 | * @param \Illuminate\Mail\Events\MessageSent $event
67 | * @return bool
68 | */
69 | protected function getQueuedStatus($event)
70 | {
71 | if (isset($event->data['__laravel_notification_queued'])) {
72 | return $event->data['__laravel_notification_queued'];
73 | }
74 |
75 | return $event->data['__telescope_queued'] ?? false;
76 | }
77 |
78 | /**
79 | * Extract the tags from the message.
80 | *
81 | * @param \Swift_Message $message
82 | * @param array $data
83 | * @return array
84 | */
85 | private function tags($message, $data)
86 | {
87 | return array_merge(
88 | array_keys($message->getTo() ?: []),
89 | array_keys($message->getCc() ?: []),
90 | array_keys($message->getBcc() ?: []),
91 | $data['__telescope'] ?? []
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/ListensForStorageOpportunities.php:
--------------------------------------------------------------------------------
1 | terminating(function () use ($app) {
44 | static::store($app[EntriesRepository::class]);
45 | });
46 | }
47 |
48 | /**
49 | * Store entries after the queue worker loops.
50 | *
51 | * @param \Illuminate\Foundation\Application $app
52 | * @return void
53 | */
54 | protected static function storeEntriesAfterWorkerLoop($app)
55 | {
56 | $app['events']->listen(JobProcessing::class, function ($event) {
57 | if ($event->connectionName !== 'sync') {
58 | static::startRecording();
59 |
60 | static::$processingJobs[] = true;
61 | }
62 | });
63 |
64 | $app['events']->listen(JobProcessed::class, function ($event) use ($app) {
65 | static::storeIfDoneProcessingJob($event, $app);
66 | });
67 |
68 | $app['events']->listen(JobFailed::class, function ($event) use ($app) {
69 | static::storeIfDoneProcessingJob($event, $app);
70 | });
71 |
72 | $app['events']->listen(JobExceptionOccurred::class, function () {
73 | array_pop(static::$processingJobs);
74 | });
75 | }
76 |
77 | /**
78 | * Store the recorded entries if totally done processing the current job.
79 | *
80 | * @param \Illuminate\Queue\Events\JobProcessed $event
81 | * @param \Illuminate\Foundation\Application $app
82 | * @return void
83 | */
84 | protected static function storeIfDoneProcessingJob($event, $app)
85 | {
86 | array_pop(static::$processingJobs);
87 |
88 | if (empty(static::$processingJobs) && $event->connectionName !== 'sync') {
89 | static::store($app[EntriesRepository::class]);
90 | static::stopRecording();
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/resources/js/base.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import moment from 'moment-timezone';
3 |
4 | export default {
5 | computed: {
6 | Telescope() {
7 | return Telescope;
8 | },
9 | },
10 |
11 | methods: {
12 | /**
13 | * Show the time ago format for the given time.
14 | */
15 | timeAgo(time) {
16 | moment.updateLocale('en', {
17 | relativeTime: {
18 | future: 'in %s',
19 | past: '%s ago',
20 | s: (number) => number + 's ago',
21 | ss: '%ds ago',
22 | m: '1m ago',
23 | mm: '%dm ago',
24 | h: '1h ago',
25 | hh: '%dh ago',
26 | d: '1d ago',
27 | dd: '%dd ago',
28 | M: 'a month ago',
29 | MM: '%d months ago',
30 | y: 'a year ago',
31 | yy: '%d years ago',
32 | },
33 | });
34 |
35 | let secondsElapsed = moment().diff(time, 'seconds');
36 | let dayStart = moment('2018-01-01').startOf('day').seconds(secondsElapsed);
37 |
38 | if (secondsElapsed > 300) {
39 | return moment(time).fromNow(true);
40 | } else if (secondsElapsed < 60) {
41 | return dayStart.format('s') + 's ago';
42 | } else {
43 | return dayStart.format('m:ss') + 'm ago';
44 | }
45 | },
46 |
47 | /**
48 | * Show the time in local time.
49 | */
50 | localTime(time) {
51 | return moment(time).local().format('MMMM Do YYYY, h:mm:ss A');
52 | },
53 |
54 | /**
55 | * Truncate the given string.
56 | */
57 | truncate(string, length = 70) {
58 | return _.truncate(string, {
59 | length: length,
60 | separator: /,? +/,
61 | });
62 | },
63 |
64 | /**
65 | * Creates a debounced function that delays invoking a callback.
66 | */
67 | debouncer: _.debounce((callback) => callback(), 500),
68 |
69 | /**
70 | * Show an error message.
71 | */
72 | alertError(message) {
73 | this.$root.alert.type = 'error';
74 | this.$root.alert.autoClose = false;
75 | this.$root.alert.message = message;
76 | },
77 |
78 | /**
79 | * Show a success message.
80 | */
81 | alertSuccess(message, autoClose) {
82 | this.$root.alert.type = 'success';
83 | this.$root.alert.autoClose = autoClose;
84 | this.$root.alert.message = message;
85 | },
86 |
87 | /**
88 | * Show confirmation message.
89 | */
90 | alertConfirm(message, success, failure) {
91 | this.$root.alert.type = 'confirmation';
92 | this.$root.alert.autoClose = false;
93 | this.$root.alert.message = message;
94 | this.$root.alert.confirmationProceed = success;
95 | this.$root.alert.confirmationCancel = failure;
96 | },
97 | },
98 | };
99 |
--------------------------------------------------------------------------------
/resources/js/screens/views/preview.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 | View
26 |
27 | {{slotProps.entry.content.name}}
28 |
29 |
30 |
31 |
32 | Path
33 |
34 | {{slotProps.entry.content.path}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Composer
60 | Type
61 |
62 |
63 |
64 |
65 |
66 | {{composer.name}}
67 |
68 |
69 | {{composer.type}}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/EntryResult.php:
--------------------------------------------------------------------------------
1 | id = $id;
87 | $this->type = $type;
88 | $this->tags = $tags;
89 | $this->batchId = $batchId;
90 | $this->content = $content;
91 | $this->sequence = $sequence;
92 | $this->createdAt = $createdAt;
93 | $this->familyHash = $familyHash;
94 | }
95 |
96 | /**
97 | * Set the URL to the entry user's avatar.
98 | *
99 | * @return $this
100 | */
101 | public function generateAvatar()
102 | {
103 | $this->avatar = Avatar::url($this->content['user'] ?? []);
104 |
105 | return $this;
106 | }
107 |
108 | /**
109 | * Get the array representation of the entry.
110 | *
111 | * @return array
112 | */
113 | public function jsonSerialize()
114 | {
115 | return collect([
116 | 'id' => $this->id,
117 | 'sequence' => $this->sequence,
118 | 'batch_id' => $this->batchId,
119 | 'type' => $this->type,
120 | 'content' => $this->content,
121 | 'tags' => $this->tags,
122 | 'family_hash' => $this->familyHash,
123 | 'created_at' => $this->createdAt->toDateTimeString(),
124 | ])->when($this->avatar, function ($items) {
125 | return $items->mergeRecursive([
126 | 'content' => [
127 | 'user' => [
128 | 'avatar' => $this->avatar,
129 | ],
130 | ],
131 | ]);
132 | })->all();
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/resources/js/screens/mail/preview.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 | Mailable
28 |
29 | {{slotProps.entry.content.mailable}}
30 |
31 |
32 | Queued
33 |
34 |
35 |
36 |
37 |
38 | From
39 |
40 | {{formatAddresses(slotProps.entry.content.from)}}
41 |
42 |
43 |
44 |
45 | To
46 |
47 | {{formatAddresses(slotProps.entry.content.to)}}
48 |
49 |
50 |
51 |
52 | Reply-To
53 |
54 | {{formatAddresses(slotProps.entry.content.replyTo)}}
55 |
56 |
57 |
58 |
59 | CC
60 |
61 | {{formatAddresses(slotProps.entry.content.cc)}}
62 |
63 |
64 |
65 |
66 | BCC
67 |
68 | {{formatAddresses(slotProps.entry.content.bcc)}}
69 |
70 |
71 |
72 |
73 | Subject
74 |
75 | {{slotProps.entry.content.subject}}
76 |
77 |
78 |
79 |
80 | Download
81 |
82 | Download .eml file
83 |
84 |
85 |
86 |
87 |
92 |
93 |
94 |
95 |
100 |
--------------------------------------------------------------------------------
/resources/js/components/Alert.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
77 |
78 |
79 |
82 |
83 |
84 |
103 |
104 |
105 |
106 |
107 |
108 |
114 |
--------------------------------------------------------------------------------
/src/Watchers/QueryWatcher.php:
--------------------------------------------------------------------------------
1 | listen(QueryExecuted::class, [$this, 'recordQuery']);
22 | }
23 |
24 | /**
25 | * Record a query was executed.
26 | *
27 | * @param \Illuminate\Database\Events\QueryExecuted $event
28 | * @return void
29 | */
30 | public function recordQuery(QueryExecuted $event)
31 | {
32 | if (! Telescope::isRecording()) {
33 | return;
34 | }
35 |
36 | $time = $event->time;
37 |
38 | if ($caller = $this->getCallerFromStackTrace()) {
39 | Telescope::recordQuery(IncomingEntry::make([
40 | 'connection' => $event->connectionName,
41 | 'bindings' => [],
42 | 'sql' => $this->replaceBindings($event),
43 | 'time' => number_format($time, 2, '.', ''),
44 | 'slow' => isset($this->options['slow']) && $time >= $this->options['slow'],
45 | 'file' => $caller['file'],
46 | 'line' => $caller['line'],
47 | 'hash' => $this->familyHash($event),
48 | ])->tags($this->tags($event)));
49 | }
50 | }
51 |
52 | /**
53 | * Get the tags for the query.
54 | *
55 | * @param \Illuminate\Database\Events\QueryExecuted $event
56 | * @return array
57 | */
58 | protected function tags($event)
59 | {
60 | return isset($this->options['slow']) && $event->time >= $this->options['slow'] ? ['slow'] : [];
61 | }
62 |
63 | /**
64 | * Calculate the family look-up hash for the query event.
65 | *
66 | * @param \Illuminate\Database\Events\QueryExecuted $event
67 | * @return string
68 | */
69 | public function familyHash($event)
70 | {
71 | return md5($event->sql);
72 | }
73 |
74 | /**
75 | * Format the given bindings to strings.
76 | *
77 | * @param \Illuminate\Database\Events\QueryExecuted $event
78 | * @return array
79 | */
80 | protected function formatBindings($event)
81 | {
82 | return $event->connection->prepareBindings($event->bindings);
83 | }
84 |
85 | /**
86 | * Replace the placeholders with the actual bindings.
87 | *
88 | * @param \Illuminate\Database\Events\QueryExecuted $event
89 | * @return string
90 | */
91 | public function replaceBindings($event)
92 | {
93 | $sql = $event->sql;
94 |
95 | foreach ($this->formatBindings($event) as $key => $binding) {
96 | $regex = is_numeric($key)
97 | ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
98 | : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
99 |
100 | if ($binding === null) {
101 | $binding = 'null';
102 | } elseif (! is_int($binding) && ! is_float($binding)) {
103 | $binding = $event->connection->getPdo()->quote($binding);
104 | }
105 |
106 | $sql = preg_replace($regex, $binding, $sql, 1);
107 | }
108 |
109 | return $sql;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Http/routes.php:
--------------------------------------------------------------------------------
1 | where('view', '(.*)')->name('telescope');
80 |
--------------------------------------------------------------------------------
/resources/js/screens/exceptions/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Type
9 | #
10 | Message
11 | Happened
12 | Resolved
13 |
14 |
15 |
16 |
17 |
18 | {{truncate(slotProps.entry.content.class, 70)}}
19 |
20 | {{truncate(slotProps.entry.content.message, 100)}}
21 |
22 |
23 |
24 | {{slotProps.entry.content.occurrences}}
25 |
26 |
27 |
28 | {{truncate(slotProps.entry.content.message, 80)}}
29 |
30 |
31 |
32 | User: {{ slotProps.entry.content.user.email }} ({{ slotProps.entry.content.user.id }})
33 |
34 |
35 |
36 | User: N/A
37 |
38 |
39 |
40 |
41 |
42 | {{timeAgo(slotProps.entry.created_at)}}
43 |
44 |
45 |
46 |
47 | {{timeAgo(slotProps.entry.content.resolved_at)}}
48 |
49 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/Storage/EntryQueryOptions.php:
--------------------------------------------------------------------------------
1 | batchId($request->batch_id)
61 | ->uuids($request->uuids)
62 | ->beforeSequence($request->before)
63 | ->tag($request->tag)
64 | ->familyHash($request->family_hash)
65 | ->limit($request->take ?? 50);
66 | }
67 |
68 | /**
69 | * Create new entry query options for the given batch ID.
70 | *
71 | * @param string $batchId
72 | * @return static
73 | */
74 | public static function forBatchId(?string $batchId)
75 | {
76 | return (new static)->batchId($batchId);
77 | }
78 |
79 | /**
80 | * Set the batch ID for the query.
81 | *
82 | * @param string $batchId
83 | * @return $this
84 | */
85 | public function batchId(?string $batchId)
86 | {
87 | $this->batchId = $batchId;
88 |
89 | return $this;
90 | }
91 |
92 | /**
93 | * Set the list of UUIDs of entries tor retrieve.
94 | *
95 | * @param array $uuids
96 | * @return $this
97 | */
98 | public function uuids(?array $uuids)
99 | {
100 | $this->uuids = $uuids;
101 |
102 | return $this;
103 | }
104 |
105 | /**
106 | * Set the ID that all retrieved entries should be less than.
107 | *
108 | * @param mixed $id
109 | * @return $this
110 | */
111 | public function beforeSequence($id)
112 | {
113 | $this->beforeSequence = $id;
114 |
115 | return $this;
116 | }
117 |
118 | /**
119 | * Set the tag that must belong to retrieved entries.
120 | *
121 | * @param string $tag
122 | * @return $this
123 | */
124 | public function tag(?string $tag)
125 | {
126 | $this->tag = $tag;
127 |
128 | return $this;
129 | }
130 |
131 | /**
132 | * Set the family hash that must belong to retrieved entries.
133 | *
134 | * @param string $familyHash
135 | * @return $this
136 | */
137 | public function familyHash(?string $familyHash)
138 | {
139 | $this->familyHash = $familyHash;
140 |
141 | return $this;
142 | }
143 |
144 | /**
145 | * Set the number of entries that should be retrieved.
146 | *
147 | * @param int $limit
148 | * @return $this
149 | */
150 | public function limit(int $limit)
151 | {
152 | $this->limit = $limit;
153 |
154 | return $this;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/Watchers/CacheWatcher.php:
--------------------------------------------------------------------------------
1 | listen(CacheHit::class, [$this, 'recordCacheHit']);
24 | $app['events']->listen(CacheMissed::class, [$this, 'recordCacheMissed']);
25 |
26 | $app['events']->listen(KeyWritten::class, [$this, 'recordKeyWritten']);
27 | $app['events']->listen(KeyForgotten::class, [$this, 'recordKeyForgotten']);
28 | }
29 |
30 | /**
31 | * Record a cache key was found.
32 | *
33 | * @param \Illuminate\Cache\Events\CacheHit $event
34 | * @return void
35 | */
36 | public function recordCacheHit(CacheHit $event)
37 | {
38 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
39 | return;
40 | }
41 |
42 | Telescope::recordCache(IncomingEntry::make([
43 | 'type' => 'hit',
44 | 'key' => $event->key,
45 | 'value' => $event->value,
46 | ]));
47 | }
48 |
49 | /**
50 | * Record a missing cache key.
51 | *
52 | * @param \Illuminate\Cache\Events\CacheMissed $event
53 | * @return void
54 | */
55 | public function recordCacheMissed(CacheMissed $event)
56 | {
57 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
58 | return;
59 | }
60 |
61 | Telescope::recordCache(IncomingEntry::make([
62 | 'type' => 'missed',
63 | 'key' => $event->key,
64 | ]));
65 | }
66 |
67 | /**
68 | * Record a cache key was updated.
69 | *
70 | * @param \Illuminate\Cache\Events\KeyWritten $event
71 | * @return void
72 | */
73 | public function recordKeyWritten(KeyWritten $event)
74 | {
75 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
76 | return;
77 | }
78 |
79 | Telescope::recordCache(IncomingEntry::make([
80 | 'type' => 'set',
81 | 'key' => $event->key,
82 | 'value' => $event->value,
83 | 'expiration' => $this->formatExpiration($event),
84 | ]));
85 | }
86 |
87 | /**
88 | * Record a cache key was forgotten / removed.
89 | *
90 | * @param \Illuminate\Cache\Events\KeyForgotten $event
91 | * @return void
92 | */
93 | public function recordKeyForgotten(KeyForgotten $event)
94 | {
95 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
96 | return;
97 | }
98 |
99 | Telescope::recordCache(IncomingEntry::make([
100 | 'type' => 'forget',
101 | 'key' => $event->key,
102 | ]));
103 | }
104 |
105 | /**
106 | * @param \Illuminate\Cache\Events\KeyWritten $event
107 | * @return mixed
108 | */
109 | protected function formatExpiration(KeyWritten $event)
110 | {
111 | return property_exists($event, 'seconds')
112 | ? $event->seconds : $event->minutes * 60;
113 | }
114 |
115 | /**
116 | * Determine if the event should be ignored.
117 | *
118 | * @param mixed $event
119 | * @return bool
120 | */
121 | private function shouldIgnore($event)
122 | {
123 | return Str::is([
124 | 'illuminate:queue:restart',
125 | 'framework/schedule*',
126 | 'telescope:*',
127 | ], $event->key);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/resources/js/screens/requests/preview.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 | Method
20 |
21 | {{slotProps.entry.content.method}}
22 |
23 |
24 |
25 |
26 | Controller Action
27 |
28 | {{slotProps.entry.content.controller_action}}
29 |
30 |
31 |
32 |
33 | Middleware
34 |
35 | {{slotProps.entry.content.middleware.join(", ")}}
36 |
37 |
38 |
39 |
40 | Path
41 |
42 | {{slotProps.entry.content.uri}}
43 |
44 |
45 |
46 |
47 | Status
48 |
49 | {{slotProps.entry.content.response_status}}
50 |
51 |
52 |
53 |
54 | Duration
55 |
56 | {{slotProps.entry.content.duration || '-'}} ms
57 |
58 |
59 |
60 |
61 | IP Address
62 |
63 | {{slotProps.entry.content.ip_address || '-'}}
64 |
65 |
66 |
67 |
68 | Memory usage
69 |
70 | {{slotProps.entry.content.memory || '-'}} MB
71 |
72 |
73 |
74 |
75 |
76 |
77 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/resources/js/screens/jobs/preview.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 | Status
32 |
33 |
34 | {{slotProps.entry.content.status}}
35 |
36 |
37 |
38 |
39 |
40 | Job
41 |
42 | {{slotProps.entry.content.name}}
43 |
44 |
45 |
46 |
47 | Connection
48 |
49 | {{slotProps.entry.content.connection}}
50 |
51 |
52 |
53 |
54 | Queue
55 |
56 | {{slotProps.entry.content.queue}}
57 |
58 |
59 |
60 |
61 | Tries
62 |
63 | {{slotProps.entry.content.tries || '-'}}
64 |
65 |
66 |
67 |
68 | Timeout
69 |
70 | {{slotProps.entry.content.timeout || '-'}}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
92 |
93 |
94 |
95 |
96 |
{{slotProps.entry.content.exception.message}}
97 |
98 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
118 |
--------------------------------------------------------------------------------
/src/Watchers/EventWatcher.php:
--------------------------------------------------------------------------------
1 | listen('*', [$this, 'recordEvent']);
27 | }
28 |
29 | /**
30 | * Record an event was fired.
31 | *
32 | * @param string $eventName
33 | * @param array $payload
34 | * @return void
35 | */
36 | public function recordEvent($eventName, $payload)
37 | {
38 | if (! Telescope::isRecording() || $this->shouldIgnore($eventName)) {
39 | return;
40 | }
41 |
42 | $formattedPayload = $this->extractPayload($eventName, $payload);
43 |
44 | Telescope::recordEvent(IncomingEntry::make([
45 | 'name' => $eventName,
46 | 'payload' => empty($formattedPayload) ? null : $formattedPayload,
47 | 'listeners' => $this->formatListeners($eventName),
48 | 'broadcast' => class_exists($eventName)
49 | ? in_array(ShouldBroadcast::class, (array) class_implements($eventName))
50 | : false,
51 | ])->tags(class_exists($eventName) && isset($payload[0]) ? ExtractTags::from($payload[0]) : []));
52 | }
53 |
54 | /**
55 | * Extract the payload and tags from the event.
56 | *
57 | * @param string $eventName
58 | * @param array $payload
59 | * @return array
60 | */
61 | protected function extractPayload($eventName, $payload)
62 | {
63 | if (class_exists($eventName) && isset($payload[0]) && is_object($payload[0])) {
64 | return ExtractProperties::from($payload[0]);
65 | }
66 |
67 | return collect($payload)->map(function ($value) {
68 | return is_object($value) ? [
69 | 'class' => get_class($value),
70 | 'properties' => json_decode(json_encode($value), true),
71 | ] : $value;
72 | })->toArray();
73 | }
74 |
75 | /**
76 | * Format list of event listeners.
77 | *
78 | * @param string $eventName
79 | * @return array
80 | */
81 | protected function formatListeners($eventName)
82 | {
83 | return collect(app('events')->getListeners($eventName))
84 | ->map(function ($listener) {
85 | $listener = (new ReflectionFunction($listener))
86 | ->getStaticVariables()['listener'];
87 |
88 | if (is_string($listener)) {
89 | return Str::contains($listener, '@') ? $listener : $listener.'@handle';
90 | } elseif (is_array($listener)) {
91 | return get_class($listener[0]).'@'.$listener[1];
92 | }
93 |
94 | return $this->formatClosureListener($listener);
95 | })->reject(function ($listener) {
96 | return Str::contains($listener, 'Laravel\\Telescope');
97 | })->map(function ($listener) {
98 | if (Str::contains($listener, '@')) {
99 | $queued = in_array(ShouldQueue::class, class_implements(explode('@', $listener)[0]));
100 | }
101 |
102 | return [
103 | 'name' => $listener,
104 | 'queued' => $queued ?? false,
105 | ];
106 | })->values()->toArray();
107 | }
108 |
109 | /**
110 | * Determine if the event should be ignored.
111 | *
112 | * @param string $eventName
113 | * @return bool
114 | */
115 | protected function shouldIgnore($eventName)
116 | {
117 | return $this->eventIsIgnored($eventName) ||
118 | (Telescope::$ignoreFrameworkEvents && $this->eventIsFiredByTheFramework($eventName));
119 | }
120 |
121 | /**
122 | * Determine if the event was fired internally by Laravel.
123 | *
124 | * @param string $eventName
125 | * @return bool
126 | */
127 | protected function eventIsFiredByTheFramework($eventName)
128 | {
129 | return Str::is(
130 | ['Illuminate\*', 'eloquent*', 'bootstrapped*', 'bootstrapping*', 'creating*', 'composing*'],
131 | $eventName
132 | );
133 | }
134 |
135 | /**
136 | * Determine if the event is ignored manually.
137 | *
138 | * @param string $eventName
139 | * @return bool
140 | */
141 | protected function eventIsIgnored($eventName)
142 | {
143 | return Str::is($this->options['ignore'] ?? [], $eventName);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/resources/js/screens/exceptions/preview.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 | Type
43 |
44 | {{slotProps.entry.content.class}}
45 |
46 |
47 |
48 |
49 | Location
50 |
51 | {{slotProps.entry.content.file}}:{{slotProps.entry.content.line}}
52 |
53 |
54 |
55 |
56 | Occurrences
57 |
58 |
59 | View Other Occurrences
60 |
61 |
62 |
63 |
64 |
65 | Resolved at
66 |
67 |
68 |
69 | {{localTime(entry.content.resolved_at)}} ({{timeAgo(entry.content.resolved_at)}})
70 |
71 |
72 | Mark as resolved
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
97 |
98 |
99 |
{{slotProps.entry.content.message}}
100 |
101 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
121 |
--------------------------------------------------------------------------------
/src/Watchers/JobWatcher.php:
--------------------------------------------------------------------------------
1 | optional($this->recordJob($connection, $queue, $payload))->uuid];
28 | });
29 |
30 | $app['events']->listen(JobProcessed::class, [$this, 'recordProcessedJob']);
31 | $app['events']->listen(JobFailed::class, [$this, 'recordFailedJob']);
32 | }
33 |
34 | /**
35 | * Record a job being created.
36 | *
37 | * @param string $connection
38 | * @param string $queue
39 | * @param array $payload
40 | * @return \Laravel\Telescope\IncomingEntry|null
41 | */
42 | public function recordJob($connection, $queue, array $payload)
43 | {
44 | if (! Telescope::isRecording()) {
45 | return;
46 | }
47 |
48 | $content = array_merge([
49 | 'status' => 'pending',
50 | ], $this->defaultJobData($connection, $queue, $payload, $this->data($payload)));
51 |
52 | Telescope::recordJob(
53 | $entry = IncomingEntry::make($content)
54 | ->tags($this->tags($payload))
55 | );
56 |
57 | return $entry;
58 | }
59 |
60 | /**
61 | * Record a queued job was processed.
62 | *
63 | * @param \Illuminate\Queue\Events\JobProcessed $event
64 | * @return void
65 | */
66 | public function recordProcessedJob(JobProcessed $event)
67 | {
68 | if (! Telescope::isRecording()) {
69 | return;
70 | }
71 |
72 | $uuid = $event->job->payload()['telescope_uuid'] ?? null;
73 |
74 | if (! $uuid) {
75 | return;
76 | }
77 |
78 | Telescope::recordUpdate(EntryUpdate::make(
79 | $uuid, EntryType::JOB, ['status' => 'processed']
80 | ));
81 | }
82 |
83 | /**
84 | * Record a queue job has failed.
85 | *
86 | * @param \Illuminate\Queue\Events\JobFailed $event
87 | * @return void
88 | */
89 | public function recordFailedJob(JobFailed $event)
90 | {
91 | if (! Telescope::isRecording()) {
92 | return;
93 | }
94 |
95 | $uuid = $event->job->payload()['telescope_uuid'] ?? null;
96 |
97 | if (! $uuid) {
98 | return;
99 | }
100 |
101 | Telescope::recordUpdate(EntryUpdate::make(
102 | $uuid, EntryType::JOB, [
103 | 'status' => 'failed',
104 | 'exception' => [
105 | 'message' => $event->exception->getMessage(),
106 | 'trace' => $event->exception->getTrace(),
107 | 'line' => $event->exception->getLine(),
108 | 'line_preview' => ExceptionContext::get($event->exception),
109 | ],
110 | ]
111 | )->addTags(['failed']));
112 | }
113 |
114 | /**
115 | * Get the default entry data for the given job.
116 | *
117 | * @param string $connection
118 | * @param string $queue
119 | * @param array $payload
120 | * @param array $data
121 | * @return array
122 | */
123 | protected function defaultJobData($connection, $queue, array $payload, array $data)
124 | {
125 | return [
126 | 'connection' => $connection,
127 | 'queue' => $queue,
128 | 'name' => $payload['displayName'],
129 | 'tries' => $payload['maxTries'],
130 | 'timeout' => $payload['timeout'],
131 | 'data' => $data,
132 | ];
133 | }
134 |
135 | /**
136 | * Extract the job "data" from the job payload.
137 | *
138 | * @param array $payload
139 | * @return array
140 | */
141 | protected function data(array $payload)
142 | {
143 | if (! isset($payload['data']['command'])) {
144 | return $payload['data'];
145 | }
146 |
147 | return ExtractProperties::from(
148 | $payload['data']['command']
149 | );
150 | }
151 |
152 | /**
153 | * Extract the tags from the job payload.
154 | *
155 | * @param array $payload
156 | * @return array
157 | */
158 | protected function tags(array $payload)
159 | {
160 | if (! isset($payload['data']['command'])) {
161 | return [];
162 | }
163 |
164 | return ExtractTags::fromJob(
165 | $payload['data']['command']
166 | );
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/resources/js/routes.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { path: '/', redirect: '/requests' },
3 |
4 | {
5 | path: '/mail/:id',
6 | name: 'mail-preview',
7 | component: require('./screens/mail/preview').default,
8 | },
9 |
10 | {
11 | path: '/mail',
12 | name: 'mail',
13 | component: require('./screens/mail/index').default,
14 | },
15 |
16 | {
17 | path: '/exceptions/:id',
18 | name: 'exception-preview',
19 | component: require('./screens/exceptions/preview').default,
20 | },
21 |
22 | {
23 | path: '/exceptions',
24 | name: 'exceptions',
25 | component: require('./screens/exceptions/index').default,
26 | },
27 |
28 | {
29 | path: '/dumps',
30 | name: 'dumps',
31 | component: require('./screens/dumps/index').default,
32 | },
33 |
34 | {
35 | path: '/logs/:id',
36 | name: 'log-preview',
37 | component: require('./screens/logs/preview').default,
38 | },
39 |
40 | {
41 | path: '/logs',
42 | name: 'logs',
43 | component: require('./screens/logs/index').default,
44 | },
45 |
46 | {
47 | path: '/notifications/:id',
48 | name: 'notification-preview',
49 | component: require('./screens/notifications/preview').default,
50 | },
51 |
52 | {
53 | path: '/notifications',
54 | name: 'notifications',
55 | component: require('./screens/notifications/index').default,
56 | },
57 |
58 | {
59 | path: '/jobs/:id',
60 | name: 'job-preview',
61 | component: require('./screens/jobs/preview').default,
62 | },
63 |
64 | {
65 | path: '/jobs',
66 | name: 'jobs',
67 | component: require('./screens/jobs/index').default,
68 | },
69 |
70 | {
71 | path: '/events/:id',
72 | name: 'event-preview',
73 | component: require('./screens/events/preview').default,
74 | },
75 |
76 | {
77 | path: '/events',
78 | name: 'events',
79 | component: require('./screens/events/index').default,
80 | },
81 |
82 | {
83 | path: '/cache/:id',
84 | name: 'cache-preview',
85 | component: require('./screens/cache/preview').default,
86 | },
87 |
88 | {
89 | path: '/cache',
90 | name: 'cache',
91 | component: require('./screens/cache/index').default,
92 | },
93 |
94 | {
95 | path: '/queries/:id',
96 | name: 'query-preview',
97 | component: require('./screens/queries/preview').default,
98 | },
99 |
100 | {
101 | path: '/queries',
102 | name: 'queries',
103 | component: require('./screens/queries/index').default,
104 | },
105 |
106 | {
107 | path: '/models/:id',
108 | name: 'model-preview',
109 | component: require('./screens/models/preview').default,
110 | },
111 |
112 | {
113 | path: '/models',
114 | name: 'models',
115 | component: require('./screens/models/index').default,
116 | },
117 |
118 | {
119 | path: '/requests/:id',
120 | name: 'request-preview',
121 | component: require('./screens/requests/preview').default,
122 | },
123 |
124 | {
125 | path: '/requests',
126 | name: 'requests',
127 | component: require('./screens/requests/index').default,
128 | },
129 |
130 | {
131 | path: '/commands/:id',
132 | name: 'command-preview',
133 | component: require('./screens/commands/preview').default,
134 | },
135 |
136 | {
137 | path: '/commands',
138 | name: 'commands',
139 | component: require('./screens/commands/index').default,
140 | },
141 |
142 | {
143 | path: '/schedule/:id',
144 | name: 'schedule-preview',
145 | component: require('./screens/schedule/preview').default,
146 | },
147 |
148 | {
149 | path: '/schedule',
150 | name: 'schedule',
151 | component: require('./screens/schedule/index').default,
152 | },
153 |
154 | {
155 | path: '/redis/:id',
156 | name: 'redis-preview',
157 | component: require('./screens/redis/preview').default,
158 | },
159 |
160 | {
161 | path: '/redis',
162 | name: 'redis',
163 | component: require('./screens/redis/index').default,
164 | },
165 |
166 | {
167 | path: '/monitored-tags',
168 | name: 'monitored-tags',
169 | component: require('./screens/monitoring/index').default,
170 | },
171 |
172 | {
173 | path: '/gates/:id',
174 | name: 'gate-preview',
175 | component: require('./screens/gates/preview').default,
176 | },
177 |
178 | {
179 | path: '/gates',
180 | name: 'gates',
181 | component: require('./screens/gates/index').default,
182 | },
183 |
184 | {
185 | path: '/views/:id',
186 | name: 'view-preview',
187 | component: require('./screens/views/preview').default,
188 | },
189 |
190 | {
191 | path: '/views',
192 | name: 'views',
193 | component: require('./screens/views/index').default,
194 | },
195 | ];
196 |
--------------------------------------------------------------------------------
/src/TelescopeServiceProvider.php:
--------------------------------------------------------------------------------
1 | registerRoutes();
28 | $this->registerMigrations();
29 | $this->registerPublishing();
30 |
31 | Telescope::start($this->app);
32 | Telescope::listenForStorageOpportunities($this->app);
33 |
34 | $this->loadViewsFrom(
35 | __DIR__.'/../resources/views', 'telescope'
36 | );
37 | }
38 |
39 | /**
40 | * Register the package routes.
41 | *
42 | * @return void
43 | */
44 | private function registerRoutes()
45 | {
46 | Route::group($this->routeConfiguration(), function () {
47 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php');
48 | });
49 | }
50 |
51 | /**
52 | * Get the Telescope route group configuration array.
53 | *
54 | * @return array
55 | */
56 | private function routeConfiguration()
57 | {
58 | return [
59 | 'domain' => config('telescope.domain', null),
60 | 'namespace' => 'Laravel\Telescope\Http\Controllers',
61 | 'prefix' => config('telescope.path'),
62 | 'middleware' => 'telescope',
63 | ];
64 | }
65 |
66 | /**
67 | * Register the package's migrations.
68 | *
69 | * @return void
70 | */
71 | private function registerMigrations()
72 | {
73 | if ($this->app->runningInConsole() && $this->shouldMigrate()) {
74 | $this->loadMigrationsFrom(__DIR__.'/Storage/migrations');
75 | }
76 | }
77 |
78 | /**
79 | * Register the package's publishable resources.
80 | *
81 | * @return void
82 | */
83 | private function registerPublishing()
84 | {
85 | if ($this->app->runningInConsole()) {
86 | $this->publishes([
87 | __DIR__.'/Storage/migrations' => database_path('migrations'),
88 | ], 'telescope-migrations');
89 |
90 | $this->publishes([
91 | __DIR__.'/../public' => public_path('vendor/telescope'),
92 | ], 'telescope-assets');
93 |
94 | $this->publishes([
95 | __DIR__.'/../config/telescope.php' => config_path('telescope.php'),
96 | ], 'telescope-config');
97 |
98 | $this->publishes([
99 | __DIR__.'/../stubs/TelescopeServiceProvider.stub' => app_path('Providers/TelescopeServiceProvider.php'),
100 | ], 'telescope-provider');
101 | }
102 | }
103 |
104 | /**
105 | * Register any package services.
106 | *
107 | * @return void
108 | */
109 | public function register()
110 | {
111 | $this->mergeConfigFrom(
112 | __DIR__.'/../config/telescope.php', 'telescope'
113 | );
114 |
115 | $this->registerStorageDriver();
116 |
117 | $this->commands([
118 | Console\ClearCommand::class,
119 | Console\InstallCommand::class,
120 | Console\PruneCommand::class,
121 | Console\PublishCommand::class,
122 | ]);
123 | }
124 |
125 | /**
126 | * Register the package storage driver.
127 | *
128 | * @return void
129 | */
130 | protected function registerStorageDriver()
131 | {
132 | $driver = config('telescope.driver');
133 |
134 | if (method_exists($this, $method = 'register'.ucfirst($driver).'Driver')) {
135 | $this->$method();
136 | }
137 | }
138 |
139 | /**
140 | * Register the package database storage driver.
141 | *
142 | * @return void
143 | */
144 | protected function registerDatabaseDriver()
145 | {
146 | $this->app->singleton(
147 | EntriesRepository::class, DatabaseEntriesRepository::class
148 | );
149 |
150 | $this->app->singleton(
151 | ClearableRepository::class, DatabaseEntriesRepository::class
152 | );
153 |
154 | $this->app->singleton(
155 | PrunableRepository::class, DatabaseEntriesRepository::class
156 | );
157 |
158 | $this->app->when(DatabaseEntriesRepository::class)
159 | ->needs('$connection')
160 | ->give(config('telescope.storage.database.connection'));
161 |
162 | $this->app->when(DatabaseEntriesRepository::class)
163 | ->needs('$chunkSize')
164 | ->give(config('telescope.storage.database.chunk'));
165 | }
166 |
167 | /**
168 | * Determine if we should register the migrations.
169 | *
170 | * @return bool
171 | */
172 | protected function shouldMigrate()
173 | {
174 | return Telescope::$runsMigrations && config('telescope.driver') === 'database';
175 | }
176 | }
177 |
--------------------------------------------------------------------------------