Skip to content

Commit 6ec2568

Browse files
committed
Update to use Laravel 12, FrankenPHP and OpenTelemetry.
1 parent 899faf7 commit 6ec2568

37 files changed

+5612
-2315
lines changed

README.md

Lines changed: 46 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ These are using Laravel Socialite, so any provider supported by Socialite can be
2020

2121
These are custom integrations but more can be added and used if people develop them. The Internal provider allows you to manually issue tickets to users.
2222

23-
## Setup
23+
24+
## Technology
25+
26+
This project is written in PHP 8.4 using the Laravel 12 framework. It was migrated from Laravel 10 and 11 so has some
27+
legacy project structure - but this is the intended upgrade path.
28+
29+
Horizon and Telescope are installed and enabled, with access limited to the admin role. The application itself is
30+
served using Laravel Octane and FrankenPHP.
31+
32+
Websocket communications are handled using Laravel Reverb.
33+
34+
## Development Setup
2435

2536
You will need to create a Discord application and have the Client ID and Client Secret available.
2637

@@ -43,108 +54,52 @@ You should now be able to login. The first user will be given the admin role.
4354

4455
## Production Deployment
4556

46-
I use the following docker-compose for running this in production:
47-
48-
```yaml
49-
version: '3'
50-
services:
51-
nginx:
52-
image: ghcr.io/mintopia/control-nginx:develop
53-
env_file: .env.nginx
54-
restart: unless-stopped
55-
depends_on:
56-
- php-fpm
57-
networks:
58-
- frontend
59-
- default
60-
volumes:
61-
- ./public:/var/www/storage/public
62-
63-
php-fpm:
64-
image: ghcr.io/mintopia/control-php-fpm:develop
65-
env_file: .env
66-
restart: unless-stopped
67-
depends_on:
68-
- redis
69-
- database
70-
volumes:
71-
- ./logs:/var/www/storage/logs
72-
- ./public:/var/www/storage/public
73-
74-
redis:
75-
image: redis:6.2.6
76-
restart: unless-stopped
77-
78-
database:
79-
image: mariadb:10.5-focal
80-
env_file: .env.mariadb
81-
restart: unless-stopped
82-
volumes:
83-
- ./database:/var/lib/mysql
84-
85-
worker:
86-
image: ghcr.io/mintopia/control-php-fpm:develop
87-
restart: unless-stopped
88-
deploy:
89-
replicas: 2
90-
env_file: .env
91-
depends_on:
92-
- database
93-
- redis
94-
volumes:
95-
- ./logs:/var/www/storage/logs
96-
- ./public:/var/www/storage/public
97-
entrypoint: ['php']
98-
command: 'artisan queue:work'
99-
100-
101-
scheduler:
102-
image: ghcr.io/mintopia/control-php-fpm:develop
103-
restart: unless-stopped
104-
env_file: .env
105-
depends_on:
106-
- database
107-
- redis
108-
volumes:
109-
- ./logs:/var/www/storage/logs
110-
- ./public:/var/www/storage/public
111-
entrypoint: ['php']
112-
command: 'artisan schedule:work'
113-
114-
artisan:
115-
image: ghcr.io/mintopia/control-php-fpm:develop
116-
profiles:
117-
- artisan
118-
env_file: .env
119-
depends_on:
120-
- database
121-
- redis
122-
volumes:
123-
- ./logs:/var/www/storage/logs
124-
- ./public:/var/www/storage/public
125-
entrypoint: ['php', 'artisan']
126-
127-
networks:
128-
frontend:
129-
external: true
130-
```
57+
In the `example` directory there is a docker compose file and some .env example files. These are for the setup I use.
58+
Just rename the .env files and edit them accordingly. You can get a [random Laravel application key here](https://generate-random.org/laravel-key-generator).
59+
60+
You need to expose the `control` container to the public. This is configured to listen on port 80
61+
in the docker compose, so you probably want something like Traefik or Caddy in-front as a reverse proxy.
62+
63+
I'm running this with an external docker network called `frontend` with Caddy running as HTTP/HTTPS ingress. You will
64+
need to add a network section for the `control` service to add it to the `frontend` network if you
65+
want to do this.
13166

132-
I'm running with an external docker network called `frontend` with Caddy running as HTTP/HTTPS ingress. To bring up the site, run the following:
67+
You will need to make a logs directory and chmod it 777 as I still need to sort permissions out.
13368

69+
To bring up the site, run the following:
13470

13571
```bash
136-
# Create your docker compose file
137-
# Create your .env file from the project's .env.example and edit as required.
13872
docker compose up -d redis database
139-
docker compose run --rm artisan key:generate
14073
docker compose run --rm artisan migrate
14174
docker compose run --rm artisan db:seed
142-
docker compose run --rm artisan control:setup-discord
75+
docker compose run --rm artisan setup:discord
14376
docker compose up -d
14477
```
14578

14679
You should now be able to visit the site and login. From here you can use the admin menu to configure the site.
14780

81+
## Observability
82+
83+
Control supports basic observability functionality in using an OpenTelemetry collector. It can support traces, logs
84+
and metrics. If enabled, it will create traces for all HTTP requests. To enable it, add the following to your `.env`:
85+
86+
```dotenv
87+
OPENTELEMETRY_ENABLED=true
88+
```
89+
90+
For logging output, a logger is defined and can be used. I suggest you use this with your usual logger, eg. `daily`.
91+
You can specify this logging with the following environment variables:
92+
93+
```dotenv
94+
LOG_CHANNEL=stack
95+
LOG_STACK=opentelemetry,daily
96+
```
97+
98+
By default it is configured to send to an OpenTelemetry container running with the name `collector`. An example config
99+
is supplied with placeholders for sending data to [Honeycomb](https://www.honeycomb.io/).
100+
101+
The plan will be to add further spans within individual requests and have spans for the jobs and queued actions.
102+
148103
## Contributing
149104

150105
It's an open source project and I'm happy to accept pull requests. I am terrible at UI and UX, which is why this is entirely using server-side rendering. If someone wants to use Vue/Laravel Livewire - please go ahead!

app/Http/Kernel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Http\Middleware\Authenticate;
66
use App\Http\Middleware\EncryptCookies;
7+
use App\Http\Middleware\MeasureRequest;
78
use App\Http\Middleware\MetricsCollector;
89
use App\Http\Middleware\PreventRequestsDuringMaintenance;
910
use App\Http\Middleware\RedirectIfAuthenticated;
@@ -41,6 +42,7 @@ class Kernel extends HttpKernel
4142
* @var array<int, class-string|string>
4243
*/
4344
protected $middleware = [
45+
MeasureRequest::class,
4446
MetricsCollector::class,
4547
// \App\Http\Middleware\TrustHosts::class,
4648
TrustProxies::class,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use App\Services\OpenTelemetry;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Route;
9+
use OpenTelemetry\SemConv\TraceAttributes;
10+
use Symfony\Component\HttpFoundation\Response;
11+
12+
class MeasureRequest
13+
{
14+
/**
15+
* Handle an incoming request.
16+
*
17+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
18+
*/
19+
public function handle(Request $request, Closure $next): Response
20+
{
21+
if (!config('open-telemetry.enabled')) {
22+
return $next($request);
23+
}
24+
25+
$name = Route::currentRouteName();
26+
if ($name === null) {
27+
$name = $request->getRequestUri();
28+
}
29+
$trace = $request->header('traceparent');
30+
$state = $request->header('tracestate');
31+
$span = OpenTelemetry::startSpan(name: "{$request->getMethod()} {$name}", parentTrace: $trace, parentState: $state);
32+
$span->span->setAttributes([
33+
TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
34+
TraceAttributes::HTTP_URL => $request->getRequestUri(),
35+
]);
36+
if (Route::getCurrentRoute() !== null) {
37+
$span->span->setAttribute(TraceAttributes::HTTP_ROUTE, Route::getCurrentRoute()->uri());
38+
}
39+
40+
$request->attributes->set('traceId', $span->span->getContext()->getTraceId());
41+
42+
$response = $next($request);
43+
$response->headers->set('X-TraceId', $span->span->getContext()->getTraceId());
44+
$span->span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
45+
$span->end();
46+
return $response;
47+
}
48+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace App\Providers;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
7+
use OpenTelemetry\Contrib\Otlp\ContentTypes;
8+
use OpenTelemetry\Contrib\Otlp\LogsExporter;
9+
use OpenTelemetry\Contrib\Otlp\MetricExporter;
10+
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
11+
use OpenTelemetry\Contrib\Otlp\SpanExporter;
12+
use OpenTelemetry\SDK\Common\Attribute\Attributes;
13+
use OpenTelemetry\SDK\Logs\LoggerProvider;
14+
use OpenTelemetry\SDK\Logs\Processor\SimpleLogRecordProcessor;
15+
use OpenTelemetry\SDK\Metrics\MeterProvider;
16+
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
17+
use OpenTelemetry\SDK\Resource\ResourceInfo;
18+
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
19+
use OpenTelemetry\SDK\Sdk;
20+
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
21+
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
22+
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
23+
use OpenTelemetry\SDK\Trace\TracerProvider;
24+
use OpenTelemetry\SemConv\ResourceAttributes;
25+
26+
class OpenTelemetryProvider extends ServiceProvider
27+
{
28+
/**
29+
* Register services.
30+
*/
31+
public function register(): void
32+
{
33+
}
34+
35+
/**
36+
* Bootstrap services.
37+
*/
38+
public function boot(): void
39+
{
40+
$this->initOpenTelemetry();
41+
}
42+
43+
protected function initOpenTelemetry(): void
44+
{
45+
$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
46+
ResourceAttributes::SERVICE_NAMESPACE => config('open-telemetry.service.namespace'),
47+
ResourceAttributes::SERVICE_NAME => config('open-telemetry.service.name'),
48+
ResourceAttributes::SERVICE_VERSION => config('open-telemetry.service.version'),
49+
ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => config('open-telemetry.environment'),
50+
ResourceAttributes::HOST_NAME => getenv('HOSTNAME'),
51+
])));
52+
$tracesEndpoint = config('open-telemetry.endpoints.traces');
53+
$spanExporter = new SpanExporter(
54+
(new OtlpHttpTransportFactory())->create($tracesEndpoint, ContentTypes::JSON)
55+
);
56+
57+
$logsEndpoint = config('open-telemetry.endpoints.logs');
58+
$logExporter = new LogsExporter(
59+
(new OtlpHttpTransportFactory())->create($logsEndpoint, ContentTypes::JSON)
60+
);
61+
62+
$metricsEndpoint = config('open-telemetry.endpoints.metrics');
63+
$reader = new ExportingReader(
64+
new MetricExporter(
65+
(new OtlpHttpTransportFactory())->create($metricsEndpoint, ContentTypes::JSON)
66+
)
67+
);
68+
69+
$meterProvider = MeterProvider::builder()
70+
->setResource($resource)
71+
->addReader($reader)
72+
->build();
73+
74+
$tracerProvider = TracerProvider::builder()
75+
->addSpanProcessor(
76+
new SimpleSpanProcessor($spanExporter)
77+
)
78+
->setResource($resource)
79+
->setSampler(new ParentBased(new AlwaysOnSampler()))
80+
->build();
81+
82+
$loggerProvider = LoggerProvider::builder()
83+
->setResource($resource)
84+
->addLogRecordProcessor(
85+
new SimpleLogRecordProcessor($logExporter)
86+
)
87+
->build();
88+
89+
Sdk::builder()
90+
->setTracerProvider($tracerProvider)
91+
->setMeterProvider($meterProvider)
92+
->setLoggerProvider($loggerProvider)
93+
->setPropagator(TraceContextPropagator::getInstance())
94+
->setAutoShutdown(true)
95+
->buildAndRegisterGlobal();
96+
}
97+
}

0 commit comments

Comments
 (0)