This is a demo service created for a presentation/lecture on modern dotnet observability.
Used technologies:
This project was built as a proof of concept for scheduling retries with Orleans Reminders at scale.
After it surved its initial purpose it became a learning playground for various new technologies like distributed tracing using OTEL.
The setup of this project requires:
- OTEL Collector to which you could pass your signals. You could also use a standalone Aspire Dashboard.
- Kafka - This project relays on a Kafaka cluster (could be 1 broker). The used topics could be found
EmailServicePoc.ServiceDefaults.Constants
. - Azure Storage Tables - At the moment Orleans in configured to use Azure Storage Tables for Grain storage and Reminders storage.
The docker-compose.yml
defines 3 OTEL related environment variables
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:5317
- OTEL_SERVICE_NAME=EmailService_API
- OTEL_RESOURCE_ATTRIBUTES=deployment.environment=development,service.namespace=EmailServicePoc,service.version=1.0.0.0-beta01
The OTEL SDK is configured to use these environment variables internally.
Kafka is configured through a Kafka:Bootstrap
configuration using the Options<T>
pattern.
The Bootstrap
property on the KafkaConfig
class is a string[]
requiring bootstrap servers and their ports, i.e. ["broker1:9092"]
.
It is possible to configure this property in the appsettings.json
or pass it as an environment variable
Kafka__Bootstrap__0="broker:9092"
Currently Orleans is configured to use Azure Table Storage as a provider. It is planned in the future to replace it with Postgres. In this case the documentation would reflect the change.
To pass the connection string it is a best practice to use the secrets.json
on your local development environment, or a secret valut in production.
The configuration key is
Azure:AzureStorageConnectionString
as before, you can define it in a json:
{
"Azure" : {
"AzureStorageConnectionString" : "redacted"
}
}
You could put it in the secrets.json
or your appsettings.json
Warning: Avoid pushing secrets into your git repo. If by mistake you did push a secret, roll it as soon as possible.
Run the API Service to get the Swagger UI to get a glimps of the API.
Before loading this service and testing behavior:
- Setup at least 1 SMTP sink server/testing server
- Prep the templates and SMTP configs in the service
The application sends real SMTP messages, meaning you'll have to spin up a sink SMTP server. I suggest:
- Mailhog - has the advantage of chaos monkey, but seems to be unmaintained.
- Mailpit - fast, great UI, lots of features, not chaos monkey.
The /email/{clientId}
endpoint sends an email to a specific address. The clientId
is used to determine through
which SMTP server the email would be sent and who would be the Sender and what would be the Sender Address.
To configure and SMTP server for a client:
POST /config/{clientId}/smtp
{
"serverUrl": "string",
"serverPort": "string",
"from": "string",
"fromName": "string",
"userName": "string",
"password": "string"
}
This will store the SMTP configuration for clientId
Sending an email requires a template. This demo service allows to define various templates using the Fluid library.
To define a template call:
POST /config/{clientId}/templates
{
"language": "string",
"type": "string",
"subjectTemplate": "string",
"bodyTemplate": "string"
}
Please use the Load.CreateConfigurations
and Load.SendEmails
projects to setup and load your installation.
- This service uses Orleans for the data access and smart caching.
- The API service has no logic it simply delegates requests into Orleans, or publishes email requests into a kafka queue.
- The sending process is split into 2 steps: Rendering and Sending. This allows to buffer rendered emails in a persistent storage (kafka) allowing the sending step to take a bit more time, without disturbing the processing and rendering of new requests.
- Send attempts that fail trigger an Orleans scheduling grain that will store the rendered email and setup a reminder to attempt to send it a at a later time.
- There is a limited amount of attempts (hard coded) after which the email is dumped.
All frameworks and libraries used are registered to output metrics and distributed traces. All observability signals are sent to an open telemetry collector using the OpenTelemetry Line Protocol (OTLP).
Note: known issue: dotnet/orleans#9052