Exemple : charges Avro et registre de schémas
Commandes Avro vérifiées sous schéma, lues en JSON ou telles quelles, format filaire du registre de schémas.
ExempleExploitationDepuis la version 1.6.0
Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la fonctionnalité côté produit : guide produit ; l'appeler : guide développeur.
Charges Avro sous schéma, registre de schémas
Une boutique publie ses commandes en Avro depuis HTTP : le corps est décodé sous order.avsc,
compilé au chargement, avant d'atteindre Kafka ; un tableau de bord les lit en SSE en JSON, un
service les lit en WebSocket en Avro tel quel. Le canal d'audit porte le format filaire de
Confluent : chaque identifiant de schéma est lu dans le registre, et doit y être enregistré sous le
sujet du tenant (<tenant>.orders.audit-value) - un autre tenant ne publie jamais sous ce schéma.
# Avro orders through the asynchronous plane (1.6.0): an HTTP caller publishes an Avro
# body - checked under its schema, compiled at load -, a subscriber over SSE or a webhook reads it in
# JSON, a WebSocket that declares binary frames reads it as it is. The audit channel carries Confluent's
# wire format: each schema id is read in the registry and must be registered under the tenant's subject.
version: 1
contracts:
openapi: ./openapi.yaml
asyncapi: ./asyncapi.yaml
upstreams:
orders-brokers:
protocol: kafka
brokers: [kafka-0.internal:9092, kafka-1.internal:9092]
schema_registry:
url: https://registry.internal:8081 # read, never written - the Confluent API
auth:
username: flexway
password: ${REGISTRY_PASSWORD} # a reference, resolved at each request
timeout: 2s # a lookup holds the message that needs it
max_schemas: 1000 # compiled once each, kept by id
auth:
issuers:
- name: customers
discovery: https://login.example.net/.well-known/openid-configuration
audience: api
tenant_claim: org# examples/async-binary/asyncapi.yaml
asyncapi: 3.0.0
info: { title: Shop orders, version: "1.0.0" }
channels:
orders:
address: "{tenant}.orders"
parameters:
tenant: { x-flexway-bind: tenant }
messages:
order:
contentType: application/vnd.apache.avro+binary
payload: { schemaFormat: "application/vnd.apache.avro;version=1.9.0", schema: { $ref: "./order.avsc" } }
audit:
address: "{tenant}.orders.audit"
parameters:
tenant: { x-flexway-bind: tenant }
messages:
audited:
contentType: application/vnd.apache.avro+binary
payload: { schemaFormat: "application/vnd.apache.avro;version=1.9.0", schema: { $ref: "./order.avsc" } }
bindings:
kafka: { schemaIdLocation: payload, schemaIdPayloadEncoding: confluent, schemaLookupStrategy: TopicIdStrategy }
operations:
publishOrder:
action: send
channel: { $ref: "#/channels/orders" }
x-flexway-upstream: orders-brokers
x-flexway-validation: full # decode every body under order.avsc - opt-in for a binary message
x-flexway-key: /orderId # read in the payload's JSON form
receiveOrder:
action: receive
channel: { $ref: "#/channels/orders" }
x-flexway-upstream: orders-brokers
publishAudit:
action: send
channel: { $ref: "#/channels/audit" }
x-flexway-upstream: orders-brokers
x-flexway-validation: full # the schema id in the registry, under <tenant>.orders.audit-value
receiveAudit:
# Records of an ordinary Kafka producer (a Confluent serializer, no CloudEvents header) are read
# too: the channel is bound to the tenant and its only message is in the registry's wire format,
# so the gateway builds each envelope - id <topic>/<partition>/<offset>, the record's timestamp.
action: receive
channel: { $ref: "#/channels/audit" }
x-flexway-upstream: orders-brokers# examples/async-binary/openapi.yaml
openapi: 3.1.0
info: { title: Shop orders, version: "1.0.0" }
components:
securitySchemes:
customers: { type: openIdConnect, openIdConnectUrl: https://login.example.net/.well-known/openid-configuration }
security: [{ customers: [] }]
paths:
/orders/events:
post:
operationId: emitOrder
x-flexway-publish: { operation: publishOrder, idempotency: required }
requestBody:
required: true
content: { application/vnd.apache.avro+binary: { schema: { type: string, format: binary } } }
responses: { "202": { description: published, after the broker's acknowledgement } }
/orders/audit:
post:
operationId: emitAudit
x-flexway-publish: { operation: publishAudit }
requestBody:
required: true
content: { application/vnd.apache.avro+binary: { schema: { type: string, format: binary } } }
responses: { "202": { description: published } }
/orders/stream:
get:
operationId: streamOrders
x-flexway-subscribe: { operation: receiveOrder, transport: [sse] }
responses: { "200": { description: the tenant's orders, in JSON } }
/orders/frames:
get:
operationId: framesOrders
x-flexway-subscribe: { operation: receiveOrder, transport: [websocket], frames: binary }
responses: { "200": { description: the tenant's orders, the Avro payload in binary frames } }
/orders/audit/stream:
get:
operationId: streamAudit
x-flexway-subscribe: { operation: receiveAudit, transport: [sse] }
responses: { "200": { description: the tenant's audit records in JSON, ordinary producers' included } }| Cas | Ce que fait la passerelle |
|---|---|
POST /orders/events, corps Avro conforme |
202 après l'accusé ; clé Kafka lue en /orderId de la forme JSON du message |
Corps qui ne se décode pas sous order.avsc |
422 invalid_request, la règle seule, jamais une valeur du message |
Autre Content-Type |
422 invalid_request (la porte ne déclare que l'Avro) ; une porte sans requestBody répond 415 unsupported_media_type |
POST /orders/audit, identifiant enregistré sous acme.orders.audit-value |
202 ; schéma lu une fois par identifiant, gardé en mémoire (max_schemas) |
| Identifiant inconnu, ou enregistré sous un autre sujet | 422 invalid_request |
| Registre injoignable ou en erreur | 503 upstream_unavailable avec Retry-After : jamais un message publié sans validation ; coupe-circuit propre au registre |
GET /orders/stream (SSE) |
Chaque évènement en JSON (encodage JSON d'Avro), datacontenttype: application/json ; un message qui ne se décode pas n'est pas remis (flexway_async_deliver_total{outcome="invalid"}) |
GET /orders/frames (WebSocket) |
Une trame texte (les attributs, dont l'id à accuser), puis une trame binaire : la charge Avro telle quelle |
Webhook (x-flexway-deliver) |
Corps CloudEvent en JSON transcodé ; registre injoignable : le message n'est ni livré ni accusé, il revient |
| Protobuf | schemaFormat: "application/vnd.google.protobuf;version=3" et schema: { $ref: "./reading.binpb#acme.Reading" } - un jeu de descripteurs compilé (protoc --descriptor_set_out --include_imports, buf build), le message par son nom ; une source .proto en ligne est refusée |
| CBOR | contentType: application/cbor et un schéma JSON : la charge est lue dans le modèle JSON de CBOR (RFC 8949 §6.1) et vérifiée contre ce schéma |
GET /orders/audit/stream, enregistrement d'un producteur Kafka ordinaire (format filaire, aucun en-tête CloudEvents) |
Remis dans l'enveloppe que construit la passerelle : id = acme.orders.audit/<partition>/<position> (stable d'une réplique à l'autre, c'est l'id SSE et le Last-Event-ID, l'id à accuser en WebSocket, le webhook-id), source = /flexway/orders-brokers/audit, type = audited, time = l'horodatage Kafka, datacontenttype: application/json après transcodage ; aucun attribut tenant : le tenant est celui du canal lié, jamais un en-tête ni le contenu du message |
Enregistrement sans attribut sur un autre canal, ou qui porte un attribut ce_* sans en être un |
Pas un évènement, comme en 1.5.0 : non remis (flexway_async_deliver_total{outcome="invalid"}) |
| Schéma d'écriture du registre différent du schéma du contrat | Résolu vers le schéma du contrat par les règles standard d'Avro (champ ajouté avec défaut, champ retiré, int -> long, alias, symbole d'énumération par défaut) : même JSON qu'un message écrit sous le schéma du contrat ; résolution une fois par paire (identifiant, schéma du contrat), max_schemas paires gardées. Schémas identiques (même forme canonique) : aucun coût de plus |
| Schéma d'écriture qui ne se résout pas vers celui du contrat (champ sans défaut, type incompatible) | Refusé entier, jamais livré en partie : 422 invalid_request à la publication ; à la consommation, non remis comme une charge qui ne se décode pas - SSE au plus une fois, WebSocket accusé, webhook mis au rebut -, la consommation continue ; compté outcome="incompatible" |
x-flexway-key sur un message binaire |
La clé est lue dans la forme JSON du message : un décodage par publication, même avec x-flexway-validation: off (une charge qui ne se décode pas est alors refusée en 422) - un coût à peser sur un débit élevé ; sans clé, une charge non validée est relayée sans être lue |
Attention
Refusé au chargement : un schemaFormat inconnu (RAML, OpenAPI, Avro 2.x), un schéma Avro
invalide, récursif ou dont un tableau contient des éléments sans octet, un message absent du jeu de
descripteurs, un $ref de schéma binaire hors du répertoire du document AsyncAPI, le format filaire
sans schema_registry ou sur un courtier qui n'est pas Kafka, x-flexway-validation: off sur un
message JSON (toujours vérifié), frames: binary sans WebSocket ou sur un message JSON, une
consommation d'un canal au format filaire isolée par le seul attribut tenant (ni liée, ni publique) :
un producteur ordinaire n'écrit aucun attribut, et le tenant n'est jamais lu dans un message.
Mesurer
flexway_async_payloads_total{operation, format, step, outcome} - step vaut validate
ou transcode, outcome ok, invalid, not_attached, incompatible ou unavailable ; jamais une valeur du
message ni un identifiant de schéma.
Lancer
Enregistrer dans un répertoire le
flexway.yamlmontré et les contrats qu'il référence, à côté de lui (examples/async-binary/dans les sources de la release).Valider hors ligne, sans courtier ni réseau - le validateur est dans l'image publiée :
docker run --rm -v "$PWD:/etc/flexway:ro" ghcr.io/flexwiz/flexway:<version> validate -vMonter les secrets que le fichier référence (
file:/etc/flexway-secrets/..., variables d'environnement), puis démarrer :docker run --rm -p 8080:8080 -p 9090:9090 \ -v "$PWD:/etc/flexway:ro" -v "$PWD/secrets:/etc/flexway-secrets:ro" `-e REGISTRY_PASSWORD` \ ghcr.io/flexwiz/flexway:<version>
Vérifier
validate -vsort en0et liste chaque opération résolue ; un refus nomme le champ fautif et sort en1.- Une fois démarrée,
curl -s localhost:9090/readyzrépond200{"status":"ready","operations":<n>}; un secret absent fait refuser le démarrage, qui le nomme. - Chaque ligne du tableau des cas se rejoue avec le développeur : Publier et s'abonner, Recevoir des webhooks signés.