|
|
@ -5,20 +5,34 @@ |
|
|
// All rights reserved. Licensed under the MIT license.
|
|
|
// All rights reserved. Licensed under the MIT license.
|
|
|
// ==========================================================================
|
|
|
// ==========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
using System; |
|
|
|
|
|
using System.Collections.Generic; |
|
|
using System.Threading.Tasks; |
|
|
using System.Threading.Tasks; |
|
|
|
|
|
using Avro; |
|
|
|
|
|
using Avro.Generic; |
|
|
using Confluent.Kafka; |
|
|
using Confluent.Kafka; |
|
|
|
|
|
using Confluent.SchemaRegistry; |
|
|
|
|
|
using Confluent.SchemaRegistry.Serdes; |
|
|
|
|
|
using GraphQL.Types; |
|
|
using Microsoft.Extensions.Options; |
|
|
using Microsoft.Extensions.Options; |
|
|
|
|
|
using Squidex.Infrastructure.Json; |
|
|
|
|
|
using Squidex.Infrastructure.Json.Objects; |
|
|
using Squidex.Infrastructure.Log; |
|
|
using Squidex.Infrastructure.Log; |
|
|
|
|
|
|
|
|
namespace Squidex.Extensions.Actions.Kafka |
|
|
namespace Squidex.Extensions.Actions.Kafka |
|
|
{ |
|
|
{ |
|
|
public sealed class KafkaProducer |
|
|
public sealed class KafkaProducer |
|
|
{ |
|
|
{ |
|
|
private readonly IProducer<string, string> producer; |
|
|
private readonly IProducer<string, string> textProducer; |
|
|
|
|
|
private readonly IProducer<string, GenericRecord> avroProducer; |
|
|
|
|
|
private readonly ISchemaRegistryClient schemaRegistry; |
|
|
|
|
|
private readonly IJsonSerializer jsonSerializer; |
|
|
|
|
|
|
|
|
public KafkaProducer(IOptions<KafkaProducerOptions> options, ISemanticLog log) |
|
|
public KafkaProducer(IOptions<KafkaProducerOptions> options, ISemanticLog log, IJsonSerializer jsonSerializer) |
|
|
{ |
|
|
{ |
|
|
producer = new ProducerBuilder<string, string>(options.Value) |
|
|
this.jsonSerializer = jsonSerializer; |
|
|
|
|
|
|
|
|
|
|
|
textProducer = new ProducerBuilder<string, string>(options.Value) |
|
|
.SetErrorHandler((p, error) => |
|
|
.SetErrorHandler((p, error) => |
|
|
{ |
|
|
{ |
|
|
LogError(log, error); |
|
|
LogError(log, error); |
|
|
@ -30,6 +44,24 @@ namespace Squidex.Extensions.Actions.Kafka |
|
|
.SetKeySerializer(Serializers.Utf8) |
|
|
.SetKeySerializer(Serializers.Utf8) |
|
|
.SetValueSerializer(Serializers.Utf8) |
|
|
.SetValueSerializer(Serializers.Utf8) |
|
|
.Build(); |
|
|
.Build(); |
|
|
|
|
|
|
|
|
|
|
|
if (options.Value.IsSchemaRegistryConfigured()) |
|
|
|
|
|
{ |
|
|
|
|
|
schemaRegistry = new CachedSchemaRegistryClient(options.Value.SchemaRegistry); |
|
|
|
|
|
|
|
|
|
|
|
avroProducer = new ProducerBuilder<string, GenericRecord>(options.Value) |
|
|
|
|
|
.SetErrorHandler((p, error) => |
|
|
|
|
|
{ |
|
|
|
|
|
LogError(log, error); |
|
|
|
|
|
}) |
|
|
|
|
|
.SetLogHandler((p, message) => |
|
|
|
|
|
{ |
|
|
|
|
|
LogMessage(log, message); |
|
|
|
|
|
}) |
|
|
|
|
|
.SetKeySerializer(Serializers.Utf8) |
|
|
|
|
|
.SetValueSerializer(new AvroSerializer<GenericRecord>(schemaRegistry, options.Value.AvroSerializer)) |
|
|
|
|
|
.Build(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private static void LogMessage(ISemanticLog log, LogMessage message) |
|
|
private static void LogMessage(ISemanticLog log, LogMessage message) |
|
|
@ -77,14 +109,80 @@ namespace Squidex.Extensions.Actions.Kafka |
|
|
.WriteProperty("reason", error.Reason)); |
|
|
.WriteProperty("reason", error.Reason)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async Task<DeliveryResult<string, string>> Send(string topicName, Message<string, string> message) |
|
|
public async Task<DeliveryResult<string, string>> Send(string topicName, Message<string, string> message, string schema) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(schema)) |
|
|
{ |
|
|
{ |
|
|
return await producer.ProduceAsync(topicName, message); |
|
|
var value = CreateAvroRecord(message.Value, schema); |
|
|
|
|
|
|
|
|
|
|
|
var avroMessage = new Message<string, GenericRecord> { Key = message.Key, Headers = message.Headers, Value = value }; |
|
|
|
|
|
|
|
|
|
|
|
await avroProducer.ProduceAsync(topicName, avroMessage); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return await textProducer.ProduceAsync(topicName, message); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private GenericRecord CreateAvroRecord(string json, string avroSchema) |
|
|
|
|
|
{ |
|
|
|
|
|
var schema = (RecordSchema)Avro.Schema.Parse(avroSchema); |
|
|
|
|
|
|
|
|
|
|
|
var jsonObject = jsonSerializer.Deserialize<JsonObject>(json); |
|
|
|
|
|
|
|
|
|
|
|
var result = (GenericRecord)GetValue(jsonObject, schema); |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public void Dispose() |
|
|
public void Dispose() |
|
|
{ |
|
|
{ |
|
|
producer?.Dispose(); |
|
|
textProducer?.Dispose(); |
|
|
|
|
|
avroProducer?.Dispose(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private object GetValue(IJsonValue value, Avro.Schema schema) |
|
|
|
|
|
{ |
|
|
|
|
|
switch (value) |
|
|
|
|
|
{ |
|
|
|
|
|
case JsonString s: |
|
|
|
|
|
return s.Value; |
|
|
|
|
|
case JsonNumber n: |
|
|
|
|
|
return n.Value; |
|
|
|
|
|
case JsonBoolean b: |
|
|
|
|
|
return b.Value; |
|
|
|
|
|
case JsonObject o: |
|
|
|
|
|
{ |
|
|
|
|
|
var recordSchema = (RecordSchema)schema; |
|
|
|
|
|
|
|
|
|
|
|
var result = new GenericRecord(recordSchema); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var (key, childValue) in o) |
|
|
|
|
|
{ |
|
|
|
|
|
if (recordSchema.TryGetField(key, out var field)) |
|
|
|
|
|
{ |
|
|
|
|
|
result.Add(key, GetValue(childValue, field.Schema)); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
case JsonArray a: |
|
|
|
|
|
{ |
|
|
|
|
|
var arraySchema = (ArraySchema)schema; |
|
|
|
|
|
|
|
|
|
|
|
var result = new List<object>(); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in a) |
|
|
|
|
|
{ |
|
|
|
|
|
result.Add(GetValue(item, arraySchema.ItemSchema)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result.ToArray(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|