I’ve been recently talking about CloudEvents and AsyncAPI,two of my favorite open-source specifications for event-driven architectures. In this blog post, I want to talk about how you can use CloudEvents and AsyncAPI together. More specifically, I’ll show you how to document CloudEvents enabled services using AsyncAPI, thanks to the flexibility and openness of both projects.
Recap: CloudEvents and AsyncAPI
Let’s first do a quick recap CloudEvents and AsyncAPI.
CloudEvents is an open-source specification for describing event data in a common way with the goal of increasing interoperability between different event systems. It has two formats: structured-mode and binary-mode.
AsyncAPI is another open source specification to define asynchronous APIs, similar to what OpenAPI (aka Swagger) does for REST APIs.
You can read more about both in my previous blog posts:
CloudEvents help to define the format of the events and AsyncAPI helps to define the event-driven APIs. If your event-driven service sends or receives CloudEvents, then you can use AsyncAPI to document that service and its CloudEvent format.
CloudEvents structured-mode
Let’s first take a look at documenting event-driven services using CloudEvents in structured-mode. As a reminder, this is how a CloudEvent looks like in structured-mode where the CloudEvent metadata (such as type, source) and the actual data are all in the same JSON format:
curl localhost:8080 -v \
-X POST \
-H "Content-Type: application/cloudevents+json" \
-d '{
"specversion": "1.0",
"type": "com.mycompany.myapp.myservice.myevent",
"source": "myservice/mysource",
"id": "1234-5678",
"time": "2023-01-02T12:34:56.789Z",
"subject": "my-important-subject",
"datacontenttype": "application/json",
"extensionattr1" : "value",
"extensionattr2" : 5,
"data": {
"foo1": "bar1",
"foo2": "bar2"
}
}'
How can you document a service receiving CloudEvents in this mode? The beauty of AsyncAPI is that it allows you to include external schemas and that’s exactly what we’ll do here. We’ll include the CloudEvents schema in AsyncAPI definition.
Here’s the AsyncAPI spec for a service accepting CloudEvents in structured-mode. Notice how you can refer to the CloudEvents spec in the payload. You can still define the data field of the CloudEvent as you want:
# Account Service in CloudEvents structured format based on this blog post:
# https://developers.redhat.com/articles/2021/06/02/simulating-cloudevents-asyncapi-and-microcks#
asyncapi: '2.6.0'
info:
title: Account Service CloudEvents - structured
version: 1.0.0
description: Processes user sign ups and publishes an event afterwards
channels:
user/signedup:
publish:
message:
$ref: '#/components/messages/userSignedUp'
components:
messages:
userSignedUp:
name: userSignedUp
title: User signed up message
summary: Emitted when a user signs up
headers:
content-type:
type: string
enum:
- 'application/cloudevents+json; charset=UTF-8'
payload:
$ref: '#/components/schemas/userSignedUpPayload'
schemas:
userSignedUpPayload:
type: object
allOf:
- $ref: 'https://raw.githubusercontent.com/cloudevents/spec/v1.0.1/spec.json'
properties:
data:
$ref: '#/components/schemas/userSignedUpData'
userSignedUpData:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
account-service-ce-structured.yaml
Take a look this in AsyncAPI Studio and you see that the service is nicely documented with an example CloudEvent structured-mode payload, nice!
CloudEvents binary-mode
In binary-mode of CloudEvents, the CloudEvent metadata is sent as headers and the data is sent in the body:
curl localhost:8080 -v \
-X POST \
-H "Content-Type: application/json" \
-H "ce-specversion: 1.0" \
-H "ce-type: com.mycompany.myapp.myservice.myevent" \
-H "ce-source: myservice/mysource" \
-H "ce-id: 1234-5678" \
-H "ce-time: 2023-01-02T12:34:56.789Z" \
-H "ce-subject: my-important-subject" \
-H "ce-extensionattr1: value" \
-H "ce-extensionattr2: 5" \
-d '{
"foo1": "bar1",
"foo2": "bar2"
}'
To document a service accepting CloudEvents in binary-mode, first, you need to define the required and optional CloudEvents headers. AsyncAPI has a useful feature called traits where you can add additional properties to operations, messages, etc.
In this case, you will first define a message trait with CloudEvent headers:
# Modified from this sample:
# https://raw.githubusercontent.com/microcks/microcks-quickstarters/main/cloud/cloudevents/cloudevents-v1.0.1-asyncapi-trait.yml
name: cloudevents-headers
summary: Message headers used by CloudEvents spec in binary content mode
headers:
type: object
required:
- ce-specversion
- ce-id
- ce-source
- ce-type
properties:
ce-specversion:
type: string
description: The version of the CloudEvents specification which the event uses.
enum:
- "1.0"
ce-id:
type: string
minLength: 1
description: Identifies the event.
ce-source:
type: string
format: uri-reference
minLength: 1
description: Identifies the context in which an event happened.
ce-type:
type: string
minLength: 1
description: Describes the type of event related to the originating occurrence.
ce-datacontenttype:
type: string
description: Content type of the data value. Must adhere to RFC 2046 format.
ce-dataschema:
type: string
description: Identifies the schema that data adheres to.
ce-subject:
type: string
description: Describes the subject of the event in the context of the event producer (identified by source).
ce-time:
type: string
format: date-time
description: Timestamp of when the occurrence happened. Must adhere to RFC 3339.
content-type:
type: string
enum:
- application/json
cloudevents-v1.0.1-asyncapi-traits.yaml
Then, you can define your service with AsyncAPI referring to the message traits you defined earlier:
# Account Service in CloudEvents binary format based on this blog post:
# https://developers.redhat.com/articles/2021/06/02/simulating-cloudevents-asyncapi-and-microcks#
asyncapi: '2.6.0'
info:
title: Account Service CloudEvents - binary
version: 1.0.0
description: Processes user sign ups and publishes an event afterwards
channels:
user/signedup:
publish:
message:
$ref: '#/components/messages/userSignedUp'
components:
messages:
userSignedUp:
name: userSignedUp
title: User signed up message
summary: Emitted when a user signs up
traits:
- $ref: 'https://raw.githubusercontent.com/meteatamel/asyncapi-basics/main/samples/account-service-cloudevents/cloudevents-v1.0.1-asyncapi-traits.yaml'
payload:
$ref: '#/components/schemas/userSignedUpPayload'
schemas:
userSignedUpPayload:
type: object
properties:
data:
$ref: '#/components/schemas/userSignedUpData'
userSignedUpData:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
account-service-ce-binary.yaml
If you take a look this in AsyncAPI Studio again, you see that the service is nicely documented with an example CloudEvent binary-mode with headers and payload:
In this blog post, I showed how to use two of my favorite open source specifications, CloudEvents and AsyncAPI, together. Thanks to these excellent blog posts that helped me in my understanding of AsyncAPI and CloudEvents: