Apitte skeleton – nelogguje errory a jen zobrazi error 500

Majksa
Člen | 17
+
+1
-

Ahoj,

začal jsem používat apitte-skeleton jako backend pro mojí aplikaci a neloggují se mi errory. Nejspíš to bude nějaká chyba v Middlewares, ale nenapadá mě co. Posílám moje soubory, ale většinou to je podobné apitte-skeletonu. Jestli je potřeba ještě něco, rád pošlu.

apitte.neon

extensions:
    middlewares: Contributte\Middlewares\DI\MiddlewaresExtension
    resource: Contributte\DI\Extension\ResourceExtension
    api: Apitte\Core\DI\ApiExtension

resource:
    resources:
        App\Module\Api\:
            paths:
                - %appDir%/Module/Api
            decorator:
                inject: true
        App\Module\Backend\:
            paths:
                - %appDir%/Module/Backend
            decorator:
                inject: true

api:
    debug: false
    plugins:
        Apitte\Core\DI\Plugin\CoreSchemaPlugin:
        Apitte\Core\DI\Plugin\CoreServicesPlugin:
        Apitte\OpenApi\DI\OpenApiPlugin:
        Apitte\Debug\DI\DebugPlugin:
        Apitte\Middlewares\DI\MiddlewaresPlugin:
            tracy: false
            autobasepath: false

services:
    middleware.tryCatch:
        create: Contributte\Middlewares\TryCatchMiddleware
        tags: [middleware: [priority: 1]]
        setup:
            - setDebugMode(%debugMode%)
            - setCatchExceptions(%productionMode%) # used in debug only
    middlewares.logging:
        create: Contributte\Middlewares\LoggingMiddleware
        arguments: [@monolog.logger.default]
        tags: [middleware: [priority: 100]]
    middleware.methodOverride:
        create: Contributte\Middlewares\MethodOverrideMiddleware
        tags: [middleware: [priority: 150]]
    middleware.authenticator:
        create: App\Model\Api\Middleware\AuthenticationMiddleware(
            App\Model\Api\Security\TokenAuthenticator(%api.bot.token%)
        )
        tags: [middleware: [priority: 200]]

    api.core.dispatcher: App\Model\Api\Dispatcher\JsonDispatcher

contributte.neon

extensions:
	console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)
	monolog: Contributte\Monolog\DI\MonologExtension

console:
	url: http://localhost/
	lazy: true

monolog:
	holder:
		enabled: true

	channel:
		default:
			handlers:
				- Monolog\Handler\RotatingFileHandler(%logDir%/syslog.log, 30, Monolog\Logger::WARNING)

			processors:
				- Monolog\Processor\WebProcessor()
				- Monolog\Processor\IntrospectionProcessor()
				- Monolog\Processor\MemoryPeakUsageProcessor()
				- Monolog\Processor\ProcessIdProcessor()

JsonDispatcher

<?php

declare(strict_types=1);

namespace App\Model\Api\Dispatcher;

use Apitte\Core\Dispatcher\JsonDispatcher as ApitteJsonDispatcher;
use Apitte\Core\Handler\IHandler;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Apitte\Core\Http\RequestAttributes;
use Apitte\Core\Router\IRouter;
use Apitte\Core\Schema\Endpoint;
use Nette\Utils\Json;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class JsonDispatcher extends ApitteJsonDispatcher
{
    /** @var SerializerInterface */
    protected SerializerInterface $serializer;

    /** @var ValidatorInterface */
    protected ValidatorInterface $validator;

    public function __construct(
        IRouter $router,
        IHandler $handler,
        SerializerInterface $serializer,
        ValidatorInterface $validator
    ) {
        parent::__construct($router, $handler);

        $this->serializer = $serializer;
        $this->validator = $validator;
    }

    protected function handle(ApiRequest $request, ApiResponse $response): ApiResponse
    {
        try {
            $request = $this->transformRequest($request);
            $result = $this->handler->handle($request, $response);

            // Except ResponseInterface convert all to json
            if (!($result instanceof ApiResponse)) {
                $response = $this->transformResponse($result, $response);
            } else {
                $response = $result;
            }
        } catch (\Apitte\Core\Exception\Api\ClientErrorException | \Apitte\Core\Exception\Api\ServerErrorException $e) {
            $data = [];

            if ($e->getMessage()) {
                $data['message'] = $e->getMessage();
            }
            if ($e->getContext()) {
                $data['context'] = $e->getContext();
            }
            if ($e->getCode()) {
                $data['code'] = $e->getCode();
            }

            $response = $response->withStatus($e->getCode() ?: 500)
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write(Json::encode($data));
        } catch (\RuntimeException $e) {
            $response = $response->withStatus($e->getCode() ?: 500)
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write(Json::encode([
                'message' => $e->getMessage() ?: 'Application encountered an internal error. Please try again later.',
            ]));
        }

        return $response;
    }

    /**
     * Transform incoming request to request DTO, if needed.
     */
    protected function transformRequest(ApiRequest $request): ApiRequest
    {
        // If Apitte endpoint is not provided, skip transforming.
        $endpoint = $request->getAttribute(RequestAttributes::ATTR_ENDPOINT);
        if (!$endpoint) {
            return $request;
        }

        // @safety
        \assert($endpoint instanceof Endpoint);

        // Get incoming request entity class, if defined. Otherwise, skip transforming.
        $entity = $endpoint->getTag('request.dto');
        if (!$entity) {
            return $request;
        }

        try {
            // Create request DTO from incoming request, using serializer.
            $dto = $this->serializer->deserialize(
                $request->getBody()->getContents(),
                $entity,
                'json',
                ['allow_extra_attributes' => false],
            );

            $request = $request->withParsedBody($dto);
        } catch (\Symfony\Component\Serializer\Exception\ExtraAttributesException $e) {
            throw \Apitte\Core\Exception\Api\ValidationException::create()
                ->withMessage($e->getMessage());
        }

        // Try to validate entity only if its enabled
        $violations = $this->validator->validate($dto);

        if (\count($violations) > 0) {
            $fields = [];
            foreach ($violations as $violation) {
                $fields[$violation->getPropertyPath()][] = $violation->getMessage();
            }

            throw \Apitte\Core\Exception\Api\ValidationException::create()
                ->withMessage('Invalid request data')
                ->withFields($fields);
        }

        return $request;
    }

    /**
     * Transform outgoing response data to JSON, if needed.
     *
     * @param mixed $data
     */
    protected function transformResponse($data, ApiResponse $response): ApiResponse
    {
        $response = $response->withStatus(200)
            ->withHeader('Content-Type', 'application/json');

        // Serialize entity with symfony/serializer to JSON
        $serialized = $this->serializer->serialize($data, 'json');

        $response->getBody()->write($serialized);

        return $response;
    }
}

AuthenticationMiddleware

<?php

declare(strict_types=1);

namespace App\Model\Api\Middleware;

use App\Model\Utils\Strings;
use Contributte\Middlewares\IMiddleware;
use Contributte\Middlewares\Security\IAuthenticator;
use Nette\Utils\Json;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class AuthenticationMiddleware implements IMiddleware
{
    private const WHITELIST_PATHS = ['/backend/v1/user/register', '/backend/v1/user/login', '/backend/v1/openapi'];

    /** @var IAuthenticator */
    private IAuthenticator $authenticator;

    public function __construct(IAuthenticator $authenticator)
    {
        $this->authenticator = $authenticator;
    }

    protected function denied(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $response->getBody()->write(Json::encode([
            'status' => 'error',
            'message' => 'Client authentication failed',
            'code' => 401,
        ]));

        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus(401);
    }

    protected function isWhitelisted(ServerRequestInterface $request): bool
    {
        foreach (self::WHITELIST_PATHS as $whitelist) {
            if (Strings::startsWith($request->getUri()->getPath(), $whitelist)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Authenticate user from given request
     */
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): ResponseInterface {
        if ($this->isWhitelisted($request)) {
            return $next($request, $response);
        }
        // If we have a identity, then go to next middlewares,
        // otherwise stop and return current response
        if (!$this->authenticator->authenticate($request)) {
            return $this->denied($request, $response);
        }

        // Pass to next middleware
        return $next($request, $response);
    }
}
Majksa
Člen | 17
+
0
-

Jenom dodám, že ostatní logování mi normálně funguje:

log/info.log

[2020-11-15 19-30-27] Requested url: http://localhost:8000/api/  @  http://localhost:8000/api/
[2020-11-15 19-30-28] Requested url: http://localhost:8000/backend/  @  http://localhost:8000/backend/
[2020-11-15 19-30-28] Requested url: http://localhost:8000/backend/v1/user/register  @  http://localhost:8000/backend/v1/user/register

log/debug.log

[2020-11-15 20-29-34] array (2) message => "Query: "START TRANSACTION"" (26) context => array (1) |  time => "1 ms" (4)  @  http://localhost:8000/backend/v1/user/register
[2020-11-15 20-29-34] array (2) message => "Query: INSERT INTO user (username, email, password, last_logged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)" (117) context => array (1) |  time => "3 ms" (4)  @  http://localhost:8000/backend/v1/user/register
[2020-11-15 20-29-34] array (2) message => "Query: "COMMIT"" (15) context => array (1) |  time => "1 ms" (4)  @  http://localhost:8000/backend/v1/user/register

Editoval Majksa (16. 11. 2020 12:48)

Majksa
Člen | 17
+
0
-

A ještě directory tree, jestli to pomůže

├───app
│   │   Bootstrap.php
│   │
│   ├───config
│   │   │   config.local.neon
│   │   │   config.local.neon.dist
│   │   │
│   │   ├───app
│   │   │       parameters.neon
│   │   │       services.neon
│   │   │
│   │   ├───env
│   │   │       base.neon
│   │   │       dev.neon
│   │   │       prod.neon
│   │   │       test.neon
│   │   │
│   │   └───ext
│   │           apitte.neon
│   │           contributte.neon
│   │           nettrine.neon
│   │
│   ├───Domain
│   │   └───Backend
│   │       ├───Facade
│   │       │       Users.php
│   │       │
│   │       └───Request
│   │               LoginUserReqDto.php
│   │               RegisterUserReqDto.php
│   │
│   ├───Model
│   │   │   App.php
│   │   │
│   │   ├───Api
│   │   │   │   RequestAttributes.php
│   │   │   │
│   │   │   ├───Dispatcher
│   │   │   │       JsonDispatcher.php
│   │   │   │
│   │   │   ├───Middleware
│   │   │   │       AuthenticationMiddleware.php
│   │   │   │
│   │   │   └───Security
│   │   │           AbstractAuthenticator.php
│   │   │           TokenAuthenticator.php
│   │   │
│   │   ├───Database
│   │   │   │   EntityManager.php
│   │   │   │   TRepositories.php
│   │   │   │
│   │   │   ├───Entity
│   │   │   │   │   AbstractEntity.php
│   │   │   │   │   User.php
│   │   │   │   │
│   │   │   │   └───Attributes
│   │   │   │           TCreatedAt.php
│   │   │   │           TId.php
│   │   │   │           TUpdatedAt.php
│   │   │   │
│   │   │   └───Repository
│   │   │           AbstractRepository.php
│   │   │           User.php
│   │   │
│   │   ├───Exception
│   │   │   │   LogicException.php
│   │   │   │   RuntimeException.php
│   │   │   │
│   │   │   ├───Logic
│   │   │   │       InvalidArgumentException.php
│   │   │   │
│   │   │   └───Runtime
│   │   │       │   AuthenticationException.php
│   │   │       │   InvalidStateException.php
│   │   │       │   IOException.php
│   │   │       │
│   │   │       ├───Database
│   │   │       │       EntityNotFoundException.php
│   │   │       │       PersistenceException.php
│   │   │       │
│   │   │       └───User
│   │   │               EmailTakenException.php
│   │   │               UsernameTakenException.php
│   │   │
│   │   ├───Security
│   │   │       Passwords.php
│   │   │
│   │   └───Utils
│   │           Arrays.php
│   │           DateTime.php
│   │           FileSystem.php
│   │           Html.php
│   │           Image.php
│   │           Strings.php
│   │           Validators.php
│   │
│   ├───Module
│   │   ├───Api
│   │   │   │   Base.php
│   │   │   │
│   │   │   └───V1
│   │   │           Base.php
│   │   │           OpenApi.php
│   │   │
│   │   └───Backend
│   │       │   Base.php
│   │       │
│   │       └───V1
│   │               Base.php
│   │               OpenApi.php
│   │               User.php
│   │
│   └───resources
│       └───tracy
│               500.json
│               500.phtml
│               500.txt
├───bin
│       console
├───db
│   ├───Fixtures
│   │       AbstractFixture.php
│   │
│   └───Migrations
│           Version20201115144710.php
├───log
│       .gitignore
│       debug.log
│       info.log
├───temp
├───tests
│   ├───api
│   ├───backend
│   ├───integration
│   │   └───config
│   ├───unit
│   ├───_data
│   ├───_output
│   ├───_support
│   └───_temp
├───vendor
└───www
        .htaccess
        index.php
        robots.txt
        web.config
Felix
Nette Core | 1186
+
0
-

Ahoj!

  1. Jsem rad, ze to stavis na/podle contributte/apitte-skeleton.
  2. Jenom se chci ujistit, kde by jsi ty errory chtel logovat?
  3. A co se deje, kdyz se neloguji?
  4. Ukazuje se error, bila stranka, nebo neco jineho?
  5. Zkousel jsi xdebug a krokovat si to?
  6. Muzes ten projekt pushnout nekam?
Majksa
Člen | 17
+
0
-

Ahoj :)

  1. Já taky. Díky, že to existuje!
  2. Errory bych chtěl logovat pomocí contributte/monolog, do nastavené cesty: takže nejspíš log/error.log nebo něco takového.
  3. +
  4. Děje se to například, když selže INSERT request do databáze při porušení unique. Vypíše to:
{
	"status": "error",
	"code": 500,
	"message": "Application encountered an internal error. Please try again later."
}

Ale nemám jak se dostat k problému, protože error není zalogován ani vypsán.
Mode je debug.

  1. Ještě ne.
  2. Zatím to mám na soukromém GitLab repozitáři ale můžu vytvořit nový public.
Majksa
Člen | 17
+
0
-

Odkaz na repozitář:
https://github.com/…tte-log-help

Majksa
Člen | 17
+
0
-

Jenom dodám, že například když je chyba v configu, tak to normálně ukáže Tracy Error page.

Sejber
Člen | 10
+
0
-

@Majksa podařilo se to nějak vyřešit?

vml
Člen | 2
+
0
-

Řešil jsem stejný problém a vyřešil jsem ho pomocí error dekorátoru. V dokumentaci zde

Chyby najdeš pak ve složce log, jak jsi zvyklý.

appite.neon

apitte:
	debug: %debugMode%
	catchException: true # Sets if exception should be catched and transformed into response or rethrown to output (debug only)

	plugins:
		Apitte\Console\DI\ConsolePlugin:
		Apitte\Debug\DI\DebugPlugin:
			debug:
				panel: %debugMode%
				negotiation: %debugMode%
		Apitte\Core\DI\Plugin\CoreDecoratorPlugin:

services:
	decorator.request.authentication:
		class: App\Model\Security\RequestAuthenticationDecorator
		tags:
			apitte.core.decorator:
				priority: 1
				type: handler.before
	decorator.error.exception:
		class: App\Model\Debug\ExceptionDecorator

ExceptionDecorator.php

<?php

namespace App\Model\Debug;

use Apitte\Core\Decorator\IErrorDecorator;
use Apitte\Core\Exception\Api\ClientErrorException;
use Apitte\Core\Exception\Api\ServerErrorException;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Throwable;
use Tracy\Debugger;
use Tracy\ILogger;

class ExceptionDecorator implements IErrorDecorator
{

	public function decorateError(ApiRequest $request, ApiResponse $response, Throwable $error): ApiResponse
	{
		Debugger::log($error, ILogger::ERROR);

		if ($error instanceof ServerErrorException) {
			$response = $response->writeJsonBody([
				'error' => 'Server error.'
			])->withStatus(500);
		}
		if ($error instanceof ClientErrorException) {
			$response = $response->writeJsonBody([
				'error' => $error->getMessage(),
				'previous' => $error->getPrevious()?->getMessage(),
			])->withStatus($error->getCode());
		}
		return $response;
	}

}

Editoval vml (6. 3. 17:12)

Felix
Nette Core | 1186
+
+1
-

Pred casem nekdo zakladal issue (https://github.com/…n/issues/664) na Githubu u apitte-skeletonu.

Pridal jsem primou ukazku https://github.com/…390a76a30db1

Dej kdyztak vedet, kdyby neco.

elnathan
Člen | 9
+
0
-

Felix napsal(a):

Pred casem nekdo zakladal issue (https://github.com/…n/issues/664) na Githubu u apitte-skeletonu.

Pridal jsem primou ukazku https://github.com/…390a76a30db1

Dej kdyztak vedet, kdyby neco.

Díky, funguje, jenom k zamyšlení: Používám v různých systémech/jazycích API Frameworky. Zde existuje základ Nette framework, který umí slušně logovat. K tomu si stáhnu knihovnu Apitte, která při chybě (viz dřívější komunikace) vrací správně JSON chybovou hlášku = to je super. Jenže vůbec nic nezaloguje (nevyužije existující Nette logování). V dokumentaci nic není, já si musím ze skeletonu úplnou náhodou díky tomuto vláknu dohledat nějaké třídy, které né že něco stačí nakonfigurovat, já je musím vytvořit, aby to začalo logovat chyby, uff … pokud dělám knihovnu pro Nette, tak by měla v základu logovat stejně jako Nette. Pokud chce někdo něco jiného, tak si to přepíše, ale ten základ musí existovat už v samotné v knihovně a né nějakém ukázkovém kódu.
Neumím si představit, že třeba ve Springu při implemetaci REST API by mi začalo vracet chybu 500 a já v logu nic nenašel. Samozřejmě výchozí je výstup do konzole (v našem případě nějaký Nette log) a pokud chci něco jiného, tak si to tam dodám, ale ten základ tam musí být a né že knihovna mlčí.

Editoval elnathan (Dnes 11:32)