Skip to content

Commit

Permalink
Merge pull request #43 from ember-nexus/feature/improve-exception-and…
Browse files Browse the repository at this point in the history
…-logging

Feature/improve exception and logging
  • Loading branch information
Syndesi authored Aug 31, 2023
2 parents 86c3d60 + d62b3d3 commit 1ec52ad
Show file tree
Hide file tree
Showing 138 changed files with 1,974 additions and 706 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###

supervisord.log
supervisord.pid
7 changes: 6 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'phpdoc_to_comment' => false
'phpdoc_to_comment' => false,
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => true,
'import_functions' => true
]
])
->setFinder($finder)
;
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Add CI workflow to check for upstream Alpine updated.
- Add supervisord to combine all relevant logs and publish to docker logs.
- Add endpoint DELETE `/token`.
### Changed
- Improve documentation.
- Change PHP CS ruleset such that global objects are always imported.
- Rework exception and logging.

## 0.0.24 - 2023-08-21
### Changed
Expand Down
7 changes: 4 additions & 3 deletions config/packages/monolog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ when@dev:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
path: "%kernel.logs_dir%/log.log"
level: debug
channels: ["!event"]
channels: ["!event", "!request"]
formatter: monolog_json_formatter
console:
type: console
process_psr_3_messages: false
Expand All @@ -26,7 +27,7 @@ when@test:
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
path: "%kernel.logs_dir%/log.log"
level: debug

when@prod:
Expand Down
3 changes: 3 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ services:

# library services

monolog_json_formatter:
class: Monolog\Formatter\JsonFormatter

Syndesi\CypherEntityManager\EventListener\OpenCypher\NodeCreateToStatementEventListener:
tags:
- name: kernel.event_listener
Expand Down
11 changes: 5 additions & 6 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,13 @@ COPY --from=nginx_unit_builder /usr/sbin/unitd /usr/sbin/unitd
COPY --from=nginx_unit_builder /usr/sbin/unitd-debug /usr/sbin/unitd-debug
COPY --from=nginx_unit_builder /usr/lib/unit/ /usr/lib/unit/
COPY --from=nginx_unit_builder /requirements.apt /requirements.apt
COPY ./docker/nginx-unit/docker-entrypoint.sh /usr/local/bin/
COPY ./docker/nginx-unit/unit-entrypoint.sh /usr/local/bin/
COPY ./docker/nginx-unit/unit.json /docker-entrypoint.d/unit.json
COPY ./docker/supervisord/supervisord.conf /etc/supervisord.conf
COPY ./docker/supervisord/docker-entrypoint.sh /usr/local/bin/
#RUN ldconfig # this command seems to not work on alpine & image works without it?
RUN set -x \
&& chmod +x /usr/local/bin/unit-entrypoint.sh \
&& chmod +x /usr/local/bin/docker-entrypoint.sh \
&& if [ -f "/tmp/libunit.a" ]; then \
mv /tmp/libunit.a /usr/lib/amd64/libunit.a; \
Expand All @@ -303,16 +306,14 @@ RUN set -x \
&& addgroup --system unit \
&& adduser -D -S -G unit unit \
&& apk update \
&& apk add curl $(cat /requirements.apt) \
&& apk add curl supervisor $(cat /requirements.apt) \
&& rm -f /requirements.apt \
&& ln -sf /dev/stdout /var/log/unit.log

STOPSIGNAL SIGTERM

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"]

WORKDIR /var/www/html


Expand Down Expand Up @@ -348,8 +349,6 @@ STOPSIGNAL SIGTERM

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"]

WORKDIR /var/www/html


Expand Down
2 changes: 1 addition & 1 deletion docker/nginx-unit/docker-entrypoint.sh → docker/nginx-unit/unit-entrypoint.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@ if [ "$1" = "unitd" ] || [ "$1" = "unitd-debug" ]; then
fi
fi

exec "$@"
exec "$@"
12 changes: 12 additions & 0 deletions docker/supervisord/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh

set -e

mkdir -p /var/www/html/var/logs
touch /var/www/html/var/logs/log.log

if [ -z "$@" ]; then
supervisord --nodaemon --configuration /etc/supervisord.conf
else
exec "$@"
fi
20 changes: 20 additions & 0 deletions docker/supervisord/supervisord.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[supervisord]
nodaemon=true
user=root

[program:nginx-unit]
command=/usr/local/bin/unit-entrypoint.sh unitd --no-daemon --control unix:/var/run/control.unit.sock
directory=/var/www/html
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
redirect_stderr=true
stdout_logfile_maxbytes=0

[program:php-logs]
command=tail -f /var/www/html/var/log/log.log
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
redirect_stderr=true
stdout_logfile_maxbytes=0
4 changes: 4 additions & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@
- Commands
- [Backup Commands](/commands/backup)
- [Database Commands](/commands/database)
- Development
- [Long Term Plans](/development/long-term-plans)
- [Best Practices](/development/best-practices)
- [Exceptions](/development/exceptions)
66 changes: 66 additions & 0 deletions docs/development/best-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Best Practices

As Ember Nexus internally uses Symfony, it also uses
[Symfony's best practices](https://symfony.com/doc/current/best_practices.html).

## Language

The source code of Ember Nexus and responses generated by Ember Nexus API should be only written in **English**.

There are two reasons for it:

- Search engines more easily find exceptions returned in a single language, helping with debugging and general
usability.
- As English is the Lingua Franca of the IT industry, it should be the primary language used in this project.

Documentations and general user guides should always be available in English, and additional translations or regional
adaptations can be created.
It should be noted that Ember Nexus strives towards long-term stability; i.e., it is better not to support language X
than to shortly abandon language X after trial usage.

When deciding between using the active vs. passive voice, asking the following question helps:

> Is it an advantage for the reader to know the performer of the action?
If the answer is Yes, use active voice. If the answer is No, use passive voice.

See [Technical Writing: Active vs. Passive voice](https://medium.com/@DaphneWatson/technical-writing-active-vs-passive-voice-485dfaa4e498)
for details.

## Coding Style

Ember Nexus API tries to implement object-orientated code with patterns where it makes sense. Duplicate code should be
reduced if the code's complexity is similar.
Using easily readable code is better than using the least amount of lines
or highly optimized code.

Refactoring parts of the source code is always possible as long as code changes are covered by unit tests and existing
feature tests are passed.

Try to keep the line length below 120 chars.

## Logging and Exceptions

Both log messages and exceptions should adhere to a few key principles:

- Messages should provide context why they are created.
- Messages should provide one specific reason, ideally a unique property or identifier.
- Messages should not contain sensitive data like passwords, hashes, and tokens.
- Messages are sentences, they end with a dot.

Log messages should be created in the following scenarios:

- Important events in the lifetime of the API should be documented. This includes startup, shutdown, and backups.
- Security events like creating or updating users, tokens, and sessions should always be logged. If possible, use the
user's unique identifier for context.
- Administrative actions should be fully logged:
- Interactions with the command line.
- Interactions with the API through users with administrative access.

Exceptions should be created in the following instances:

- If input validation returns an error.
- If an action is forbidden, likely due to missing security privileges.
- If the requested data does not exist.
- If there is a logic conflict.
- In other problematic instances.
25 changes: 25 additions & 0 deletions docs/development/exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Exceptions

List of all exceptions:

- Internal exceptions: Thrown if there is a problem within the API, 5xx error codes
- External exceptions: Thrown if requests from the client are in any way problematic, 4xx error codes.
- `400-bad-request`: Thrown if user supplied data is bad, incomplete, or problematic.
- `401-unauthorized`: Thrown if supplied token is bad, or anonymous user does not exist.
- `404-not-found`: Default error if data does not exist, or user has no permission to read it.
- `405-method-not-allowed`: Returned if user has READ access, but access to current method.

| HTTP Status Code | Type | Title | Example Detail (prod) | Example Detail (dev) |
| ---------------- | ----------------------------------------- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 400 | `/error/400/missing-property` | Missing property | Endpoint requires that the request contains property 'x' to be set to Y. | Endpoint requires that the request contains property 'x' to be set to Y. |
| 400 | `/error/400/forbidden-property` | Forbidden Property | Endpoint does not accept setting the property 'x' in the request. | Endpoint does not accept setting the property 'x' in the request. |
| 400 | `/error/400/incomplete-mutual-dependency` | Incomplete mutual dependency | Endpoint has mutual dependency on properties 'x' and 'y'. While property 'x' is set, property 'y' is missing. | Endpoint has mutual dependency on properties 'x' and 'y'. While property 'x' is set, property 'y' is missing. |
| 400 | `/error/400/reserved-identifier` | Reserved identifier | The requested identifier 'x' is reserved and can not be used. | The requested identifier 'x' is reserved and can not be used. |
| 400 | `/error/400/bad-content` | Bad content | Endpoint expects property 'x' to be Y, got 'z'. | Endpoint expects property 'x' to be Y, got 'z'. |
| 401 | `/error/401/unauthorized` | Unauthorized | Request does not contain valid token. | Request does not contain valid token. |
| 404 | `/error/404/not-found` | Not found | Requested element was not found. | Requested element was not found. |
| 405 | `/error/405/method-not-allowed` | Method not allowed | Endpoint does not support requested method, or you do not have sufficient permissions. | Endpoint does not support requested method, or you do not have sufficient permissions. |
| 429 | `/error/429/too-many-requests` | Too many requests | You have sent too many requests, please slow down. | You have sent too many requests, please slow down. |
| 500 | `/error/500/internal-server-error` | Internal server error | Internal server error, see log. | 'error message'. |
| 501 | `/error/501/not-implemented` | Not implemented | Endpoint is currently not implemented. | Endpoint is currently not implemented. |
| 503 | `/error/503/service-unavailable` | Service unavailable | The service itself or an internal component is currently unavailable. | Service 'x' is currently unavailable. |
24 changes: 24 additions & 0 deletions docs/development/long-term-plans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Long-Term Plans

The Ember Nexus API is not intended to be modified or extended by third parties directly - we do not guarantee that
internal code is not refactored or creates breaking changes in minor version updates.

If you need to handle custom business logic or general automation, then it is recommended to create a separate
web app, which communicates with Ember Nexus over its API, which is stable and will not have breaking changes in minor
version updates.

## Version 2 of Ember Nexus API

While no guarantees can be given, we want to experiment if rewriting Ember Nexus API in Rust provides significant
performance benefits.
This experiment starts after the release of version 1.0.0 and will be covered in separate blog
posts.

The API endpoints of version 2.0, regardless if the API uses Rust or not, will likely be highly similar.

## Native Support for HTTP 2 / HTTP 3

Neither HTTP 2 nor HTTP 3 will be supported during version 1.x. It is recommended to use a reverse proxy like
[Traefik](https://traefik.io/traefik/).

Support for version 2.x is likely.
44 changes: 44 additions & 0 deletions docs/getting-started/hardware-requirements.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Hardware Requirements

Computer Architectures

The Ember Nexus API stack currently supports the following CPU architectures:

| Software | amd64 | arm64 | riscv64 | ppc64le | s390x | mips64le |
| --------------- | ----- | ----- | ------- | ------- | ----- | -------- |
| **Ember Nexus API** | ✅ <sup>1</sup> | ✅ <sup>2</sup> | 🚧 <sup>3</sup> | ⛔ <sup>4</sup> | ⛔ <sup>4</sup> | ⛔ <sup>4</sup> |
| Neo4j | ✅ <sup>5</sup> | ✅ <sup>5</sup> | 🚧 <sup>6</sup> | 🚧 <sup>6</sup> | 🚧 <sup>6</sup> | ⛔ <sup>7</sup> |
| MongoDB | ✅ <sup>8</sup> | ✅ <sup>8</sup> | ⛔ <sup>7</sup> | ✅ <sup>9</sup> | ✅ <sup>9</sup> | ⛔ <sup>7</sup> |
| Elasticsearch | ✅ <sup>10</sup> | ✅ <sup>10</sup> | 🚧 <sup>6</sup> | 🚧 <sup>6</sup> | 🚧 <sup>6</sup> | ⛔ <sup>7</sup> |
| Redis | ✅ <sup>11</sup> | ✅ <sup>11</sup> | 🚧 <sup>12</sup> | ✅ <sup>11</sup> | ✅ <sup>11</sup> | ✅ <sup>11</sup> |
| RabbitMQ | ✅ <sup>13</sup> | ✅ <sup>13</sup> | ⛔ <sup>14</sup> | ✅ <sup>13</sup> | ✅ <sup>13</sup> | ⛔ <sup>7</sup> |
| MinIO | ✅ <sup>15</sup> | ✅ <sup>15</sup> | ⛔ <sup>16</sup> | ✅ <sup>15</sup> | ✅ <sup>15</sup> | ✅ <sup>17</sup> |

1: The architecture amd64 is supported by default.
2: The architecture arm64 is supported since 0.0.23.
3: The architecture riscv64 will likely be supported once a) computers and CI/CD infrastructure get more available and
b) upstream dependencies get official support; see also [PHP](https://github.com/docker-library/php/issues/1279) and
[NGINX Unit](https://github.com/nginx/unit/issues/926).
4: No support is planned, although you can ask for it by opening a [GitHub issue](https://github.com/ember-nexus/api/issues).
It likely requires hardware donation or general collaboration.
5: Neo4j [officially supports](https://neo4j.com/docs/operations-manual/current/installation/requirements/)
amd64 and arm64.
6: No official support, although Java applications should be able to run on these architectures.
7: Software is not runnable on this architecture.
8: MongoDB [officially supports](https://www.mongodb.com/docs/manual/installation/#supported-platforms) amd64 and arm64
in their community and enterprise versions.
9: MongoDB supports these architectures in
[enterprise-only versions](https://www.mongodb.com/docs/manual/installation/#supported-platforms).
10: Elasticsearch [officially supports](https://www.elastic.co/support/matrix) amd64 and arm64.
11: Redis [supports most architectures](https://hub.docker.com/_/redis).
12: Redis seems to be [experimentally runable](https://github.com/redis/redis/pull/12349) on RISC V.
13: RabbitMQ supports [most platforms Erlang covers](https://www.rabbitmq.com/platforms.html).
14: RabbitMQ [might support](https://www.rabbitmq.com/platforms.html) RISC V once it is
[supported by Erlang](https://github.com/erlang/otp/issues/7498).
15: MinIO [supports](https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-single-node-multi-drive.html#download-the-minio-server)
amd64, arm64, ppc64le as well as s390x.
16: MinIO [does not support](https://github.com/minio/minio/pull/17161) RISC V yet, although there is some community
work done.
17: MinIO [seems to support](https://github.com/minio/minio/blob/adb8be069ee18f5360c2a9dcd22054b113493fec/buildscripts/cross-compile.sh#L12C34-L12C44)
mips64, although it is not available through Docker.

If this information needs to be corrected, please open a [GitHub issue](https://github.com/ember-nexus/api/issues).

## Local and Development Setups

For local and development purposes, the whole stack can be hosted on a single machine with at least 8 GB of RAM,
Expand Down
18 changes: 10 additions & 8 deletions lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace EmberNexusBundle\Service;

use Exception;

class EmberNexusConfiguration
{
public const PAGE_SIZE = 'pageSize';
Expand Down Expand Up @@ -43,7 +45,7 @@ private static function getValueFromConfig(array $configuration, array $keyParts
foreach ($keyParts as $i => $keyPart) {
$currentKeyParts[] = $keyPart;
if (!array_key_exists($keyPart, $configuration)) {
throw new \Exception(sprintf("Configuration must contain key '%s'.", implode('.', $currentKeyParts)));
throw new Exception(sprintf("Configuration must contain key '%s'.", implode('.', $currentKeyParts)));
}
$configuration = $configuration[$keyPart];
}
Expand Down Expand Up @@ -78,13 +80,13 @@ public static function createFromConfiguration(array $configuration): self
));

if ($emberNexusConfiguration->getPageSizeMax() < $emberNexusConfiguration->getPageSizeMin()) {
throw new \Exception('pagesize max must be smaller or equal to pagesize min.');
throw new Exception('pagesize max must be smaller or equal to pagesize min.');
}
if ($emberNexusConfiguration->getPageSizeDefault() < $emberNexusConfiguration->getPageSizeMin()) {
throw new \Exception('default page size must be at least as big as min pagesize');
throw new Exception('default page size must be at least as big as min pagesize');
}
if ($emberNexusConfiguration->getPageSizeMax() < $emberNexusConfiguration->getPageSizeDefault()) {
throw new \Exception('default page size must be equal or less than max page size.');
throw new Exception('default page size must be equal or less than max page size.');
}

$emberNexusConfiguration->setRegisterEnabled((bool) self::getValueFromConfig(
Expand Down Expand Up @@ -164,14 +166,14 @@ public static function createFromConfiguration(array $configuration): self

if (false !== $emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) {
if ($emberNexusConfiguration->getTokenMaxLifetimeInSeconds() < $emberNexusConfiguration->getTokenMinLifetimeInSeconds()) {
throw new \Exception('token max lifetime must be longer than min lifetime.');
throw new Exception('token max lifetime must be longer than min lifetime.');
}
if ($emberNexusConfiguration->getTokenDefaultLifetimeInSeconds() > $emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) {
throw new \Exception('Token default lifetime must by shorter or equal to max lifetime.');
throw new Exception('Token default lifetime must by shorter or equal to max lifetime.');
}
}
if ($emberNexusConfiguration->getTokenDefaultLifetimeInSeconds() < $emberNexusConfiguration->getTokenMinLifetimeInSeconds()) {
throw new \Exception('token default lifetime must be equal or longer to min lifetime.');
throw new Exception('token default lifetime must be equal or longer to min lifetime.');
}

return $emberNexusConfiguration;
Expand Down Expand Up @@ -233,7 +235,7 @@ public function getRegisterUniqueIdentifier(): string
public function setRegisterUniqueIdentifier(string $registerUniqueIdentifier): EmberNexusConfiguration
{
if (0 === strlen($registerUniqueIdentifier)) {
throw new \Exception('Unique identifier can not be an empty string.');
throw new Exception('Unique identifier can not be an empty string.');
}
$this->registerUniqueIdentifier = $registerUniqueIdentifier;

Expand Down
Loading

0 comments on commit 1ec52ad

Please sign in to comment.